From 03d8e15eeb1efdcac54036aadb1a5bce2d7cbbdb Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sun, 2 Nov 2025 20:41:13 +0100 Subject: [PATCH] Improve iOS quick passkey autofill to work on iOS 18+ --- .../CredentialProviderViewController.swift | 86 ++++++++++--------- .../ios/NativeVaultManager/VaultManager.swift | 13 +-- .../ios/VaultStoreKit/VaultStore+Crypto.swift | 4 + 3 files changed, 52 insertions(+), 51 deletions(-) diff --git a/apps/mobile-app/ios/Autofill/CredentialProviderViewController.swift b/apps/mobile-app/ios/Autofill/CredentialProviderViewController.swift index b85411bfe..8405563b5 100644 --- a/apps/mobile-app/ios/Autofill/CredentialProviderViewController.swift +++ b/apps/mobile-app/ios/Autofill/CredentialProviderViewController.swift @@ -228,52 +228,58 @@ public class CredentialProviderViewController: ASCredentialProviderViewControlle // If we're in quick return mode, now trigger the unlock and complete the request // The loading view is already visible from viewWillAppear if isQuickReturnMode { - let vaultStore = VaultStore() + // Dispatch async to ensure the view is fully rendered before showing biometric prompt + // This prevents a race condition where the first tap doesn't trigger the biometric UI + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } - if !sanityChecks(vaultStore: vaultStore) { - return - } + let vaultStore = VaultStore() - // Check if biometric authentication is available - if !vaultStore.isBiometricAuthEnabled() { - print("Quick return failed: Biometric auth not enabled") - self.extensionContext.cancelRequest(withError: NSError( - domain: ASExtensionErrorDomain, - code: ASExtensionError.failed.rawValue, - userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("biometric_auth_required_message", comment: "Please enable Face ID in the main AliasVault app to use autofill.")] - )) - return - } - - do { - try vaultStore.unlockVault() - - if let passkeyRequest = quickReturnPasskeyRequest { - handleQuickReturnPasskeyCredential(vaultStore: vaultStore, request: passkeyRequest) - } else if let passwordRequest = quickReturnPasswordRequest { - handleQuickReturnPasswordCredential(vaultStore: vaultStore, request: passwordRequest) + if !self.sanityChecks(vaultStore: vaultStore) { + return } - } 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 9: - errorMessage = NSLocalizedString("keychain_error_message", comment: "Failed to retrieve encryption key. This may be due to cancelled biometric authentication.") - default: - break + // Check if biometric authentication is available + if !vaultStore.isBiometricAuthEnabled() { + print("Quick return failed: Biometric auth not enabled") + self.extensionContext.cancelRequest(withError: NSError( + domain: ASExtensionErrorDomain, + code: ASExtensionError.failed.rawValue, + userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("biometric_auth_required_message", comment: "Please enable Face ID in the main AliasVault app to use autofill.")] + )) + return + } + + do { + try vaultStore.unlockVault() + + 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)") - self.extensionContext.cancelRequest(withError: NSError( - domain: ASExtensionErrorDomain, - code: ASExtensionError.failed.rawValue, - userInfo: [NSLocalizedDescriptionKey: errorMessage] - )) + // 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 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] + )) + } } } } diff --git a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift index 3067ab56c..2798942d3 100644 --- a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift +++ b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift @@ -452,17 +452,8 @@ public class VaultManager: NSObject { // Get all credentials from the vault let credentials = try vaultStore.getAllCredentials() - if #available(iOS 26.0, *) { - // iOS 26+: Register both passwords and passkeys for QuickType and manual selection - try await CredentialIdentityStore.shared.saveCredentialIdentities(credentials) - } else { - // iOS 17 and 18: Only register passkeys (skip passwords for QuickType as biometric unlock is buggy on these versions) - let passkeyOnlyCredentials = credentials.filter { credential in - guard let passkeys = credential.passkeys else { return false } - return !passkeys.isEmpty - } - try await CredentialIdentityStore.shared.saveCredentialIdentities(passkeyOnlyCredentials) - } + // Register both passwords and passkeys for QuickType and manual selection + try await CredentialIdentityStore.shared.saveCredentialIdentities(credentials) await MainActor.run { resolve(nil) diff --git a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Crypto.swift b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Crypto.swift index 85d3359e6..6eadeca55 100644 --- a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Crypto.swift +++ b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Crypto.swift @@ -222,6 +222,10 @@ extension VaultStore { context.interactionNotAllowed = false context.localizedReason = "Authenticate to unlock your vault" + // Add a small delay to ensure the context is fully ready + // This helps prevent race conditions where the biometric prompt doesn't show on first tap + Thread.sleep(forTimeInterval: 0.05) + let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: VaultConstants.keychainService,