From 16a858ee089a855c81cfbcef5fb680be1163f88c Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 28 Apr 2025 13:34:32 +0200 Subject: [PATCH] Add iOS suggestion credential insert scaffolding (#771) --- .../Autofill/CredentialIdentityStore.swift | 54 +++++++++++++++---- .../CredentialProviderViewController.swift | 21 +++++++- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/mobile-app/ios/Autofill/CredentialIdentityStore.swift b/mobile-app/ios/Autofill/CredentialIdentityStore.swift index 0a4cc8705..afa24b7e4 100644 --- a/mobile-app/ios/Autofill/CredentialIdentityStore.swift +++ b/mobile-app/ios/Autofill/CredentialIdentityStore.swift @@ -14,21 +14,41 @@ class CredentialIdentityStore { private init() {} func saveCredentialIdentities(_ credentials: [Credential]) async throws { - let identities = credentials.map { credential in - let serviceIdentifier = ASCredentialServiceIdentifier( - identifier: credential.service.name ?? "", - type: .domain - ) + let identities: [ASPasswordCredentialIdentity] = credentials.compactMap { credential in + guard let urlString = credential.service.url, + let url = URL(string: urlString), + let host = url.host else { + return nil + } + guard let username = credential.username, !username.isEmpty else { + return nil + } + + let effectiveDomain = Self.effectiveDomain(from: host) return ASPasswordCredentialIdentity( - serviceIdentifier: serviceIdentifier, - user: credential.username ?? "", - // TODO: Use the actual record identifier when implementing the actual vault - recordIdentifier: UUID().uuidString + serviceIdentifier: ASCredentialServiceIdentifier(identifier: effectiveDomain, type: .domain), + user: username, + recordIdentifier: credential.id.uuidString ) } - try await store.saveCredentialIdentities(identities) + guard !identities.isEmpty else { + print("No valid identities to save.") + return + } + + let state = await storeState() + guard state.isEnabled else { + print("Credential identity store is not enabled.") + return + } + + do { + try await store.saveCredentialIdentities(identities) + } catch { + print("Failed to save credential identities to native iOS storage: \(error)") + } } func removeAllCredentialIdentities() async throws { @@ -52,4 +72,18 @@ class CredentialIdentityStore { try await store.removeCredentialIdentities(identities) } + + private func storeState() async -> ASCredentialIdentityStoreState { + await withCheckedContinuation { continuation in + store.getState { state in + continuation.resume(returning: state) + } + } + } + + private static func effectiveDomain(from host: String) -> String { + let parts = host.split(separator: ".") + guard parts.count >= 2 else { return host } + return parts.suffix(2).joined(separator: ".") + } } diff --git a/mobile-app/ios/Autofill/CredentialProviderViewController.swift b/mobile-app/ios/Autofill/CredentialProviderViewController.swift index c54982c4a..3e838ec4f 100644 --- a/mobile-app/ios/Autofill/CredentialProviderViewController.swift +++ b/mobile-app/ios/Autofill/CredentialProviderViewController.swift @@ -29,7 +29,9 @@ class CredentialProviderViewController: ASCredentialProviderViewController { let viewModel = CredentialProviderViewModel( loader: { try VaultStore.shared.initializeDatabase() - return try VaultStore.shared.getAllCredentials() + let credentials = try VaultStore.shared.getAllCredentials() + await self.registerCredentialIdentities(credentials: credentials) + return credentials }, selectionHandler: { [weak self] credential in guard let self = self else { return } @@ -85,7 +87,10 @@ class CredentialProviderViewController: ASCredentialProviderViewController { override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { do { let credentials = try VaultStore.shared.getAllCredentials() - if let matchingCredential = credentials.first(where: { $0.service.name ?? "" == credentialIdentity.serviceIdentifier.identifier }) { + + if let matchingCredential = credentials.first(where: { credential in + return credential.id.uuidString == credentialIdentity.recordIdentifier + }) { let passwordCredential = ASPasswordCredential( user: matchingCredential.username ?? "", password: matchingCredential.password?.value ?? "" @@ -109,4 +114,16 @@ class CredentialProviderViewController: ASCredentialProviderViewController { ) } } + + /** + * This registers all known AliasVault credentials into iOS native credential storage, which iOS can then use to suggest autofill credentials when a user + * focuses an input field on a login form. These suggestions will then be shown above the iOS keyboard, which saves the user one step. + */ + private func registerCredentialIdentities(credentials: [Credential]) async { + do { + try await CredentialIdentityStore.shared.saveCredentialIdentities(credentials) + } catch { + print("Failed to save credential identities: \(error)") + } + } }