diff --git a/mobile-app/ios/Autofill/CredentialProviderViewController.swift b/mobile-app/ios/Autofill/CredentialProviderViewController.swift index a0e8243df..c54982c4a 100644 --- a/mobile-app/ios/Autofill/CredentialProviderViewController.swift +++ b/mobile-app/ios/Autofill/CredentialProviderViewController.swift @@ -20,6 +20,8 @@ import VaultModels * logins in the keyboard). */ class CredentialProviderViewController: ASCredentialProviderViewController { + private var viewModel: CredentialProviderViewModel? + override func viewDidLoad() { super.viewDidLoad() @@ -46,6 +48,8 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } ) + self.viewModel = viewModel + let hostingController = UIHostingController( rootView: CredentialProviderView(viewModel: viewModel) ) @@ -65,7 +69,13 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { - // This is handled in the SwiftUI view's onAppear + guard let viewModel = self.viewModel else { return } + + // Instead of directly filtering credentials, just set the search text + let matchedDomains = serviceIdentifiers.map { $0.identifier.lowercased() } + if let firstDomain = matchedDomains.first { + viewModel.setSearchFilter(firstDomain) + } } override func prepareInterfaceForUserChoosingTextToInsert() { @@ -73,11 +83,6 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { - // Get credentials and return the first one that matches the identity - // TODO: how do we handle authentication here? We need Face ID access before we can access credentials.. - // so we probably actually need to have a .shared instance in the autofill extension where after one unlock - // it stays unlocked? Or should we cache usernames locally and still require faceid as soon as user tries to - // autofill the username? Check how this should work functionally. do { let credentials = try VaultStore.shared.getAllCredentials() if let matchingCredential = credentials.first(where: { $0.service.name ?? "" == credentialIdentity.serviceIdentifier.identifier }) { diff --git a/mobile-app/ios/VaultUI/Components/CredentialCardView.swift b/mobile-app/ios/VaultUI/Components/CredentialCardView.swift index 49c38d763..8e274ce6a 100644 --- a/mobile-app/ios/VaultUI/Components/CredentialCardView.swift +++ b/mobile-app/ios/VaultUI/Components/CredentialCardView.swift @@ -20,11 +20,11 @@ struct CredentialCard: View { .frame(width: 32, height: 32) VStack(alignment: .leading, spacing: 4) { - Text(credential.service.name ?? "Unknown Service") + Text(truncateText(credential.service.name ?? "Unknown", limit: 26)) .font(.headline) .foregroundColor(colorScheme == .dark ? ColorConstants.Dark.text : ColorConstants.Light.text) - Text(credential.username ?? "No username") + Text(truncateText(usernameOrEmail(credential: credential), limit: 26)) .font(.subheadline) .foregroundColor(colorScheme == .dark ? ColorConstants.Dark.textMuted : ColorConstants.Light.textMuted) } @@ -47,6 +47,25 @@ struct CredentialCard: View { } } +func usernameOrEmail(credential: Credential) -> String { + if let username = credential.username, !username.isEmpty { + return username + } + if let email = credential.alias?.email, !email.isEmpty { + return email + } + return "" +} + +func truncateText(_ text: String?, limit: Int) -> String { + guard let text = text else { return "" } + if text.count > limit { + let index = text.index(text.startIndex, offsetBy: limit) + return String(text[.. String? { + guard let url = URL(string: urlString), let host = url.host else { return nil } + let parts = host.components(separatedBy: ".") + return parts.count >= 2 ? parts.suffix(2).joined(separator: ".") : host + } + + func extractDomainWithoutExtension(from domain: String) -> String { + return domain.components(separatedBy: ".").first ?? domain + } + + if let searchUrl = URL(string: searchText), let hostname = searchUrl.host, !hostname.isEmpty { + let baseUrl = "\(searchUrl.scheme ?? "https")://\(hostname)" + let rootDomain = extractRootDomain(from: searchUrl.absoluteString) ?? hostname + let domainWithoutExtension = extractDomainWithoutExtension(from: rootDomain) + + // 1. Exact URL match + var matches = credentials.filter { credential in + if let serviceUrl = credential.service.url, + let url = URL(string: serviceUrl) { + return url.absoluteString.lowercased() == searchUrl.absoluteString.lowercased() + } + return false + } + + // 2. Base URL match (excluding query/path) + if matches.isEmpty { + matches = credentials.filter { credential in + if let serviceUrl = credential.service.url, + let url = URL(string: serviceUrl) { + return url.absoluteString.lowercased().hasPrefix(baseUrl.lowercased()) + } + return false + } + } + + // 3. Root domain match (e.g., coolblue.nl) + if matches.isEmpty { + matches = credentials.filter { credential in + if let serviceUrl = credential.service.url, + let credRootDomain = extractRootDomain(from: serviceUrl) { + return credRootDomain.lowercased() == rootDomain.lowercased() + } + return false + } + } + + // 4. Domain name part match (e.g., "coolblue" in service name) + if matches.isEmpty { + matches = credentials.filter { credential in + if let serviceName = credential.service.name?.lowercased() { + return serviceName.contains(domainWithoutExtension.lowercased()) || + domainWithoutExtension.lowercased().contains(serviceName) + } + return false + } + } + + filteredCredentials = matches } else { + // Non-URL fallback: simple text search in service name or username filteredCredentials = credentials.filter { credential in (credential.service.name?.localizedCaseInsensitiveContains(searchText) ?? false) || (credential.username?.localizedCaseInsensitiveContains(searchText) ?? false) @@ -341,6 +437,25 @@ class PreviewCredentialProviderViewModel: CredentialProviderViewModel { createdAt: Date(), updatedAt: Date(), isDeleted: false + ), + Credential( + id: UUID(), + alias: .preview, + service: Service( + id: UUID(), + name: "Long name service with a lot of characters", + url: "https://another.com", + logo: nil, + createdAt: Date(), + updatedAt: Date(), + isDeleted: false + ), + username: "usernameisalsoprettylongjusttoseewhathappens", + notes: "Another sample credential", + password: .preview, + createdAt: Date(), + updatedAt: Date(), + isDeleted: false ) ]