From a37052e4dc42d8b6b30b4bde47cdd4a41edfcd8d Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 28 Apr 2025 16:07:19 +0200 Subject: [PATCH] Add actionsheet when selecting autofill credential to choose username or email (#771) --- .../CredentialProviderViewController.swift | 20 +++--- .../Views/CredentialProviderView.swift | 64 +++++++++++++++++-- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/mobile-app/ios/Autofill/CredentialProviderViewController.swift b/mobile-app/ios/Autofill/CredentialProviderViewController.swift index 3e838ec4f..64d10761c 100644 --- a/mobile-app/ios/Autofill/CredentialProviderViewController.swift +++ b/mobile-app/ios/Autofill/CredentialProviderViewController.swift @@ -33,11 +33,11 @@ class CredentialProviderViewController: ASCredentialProviderViewController { await self.registerCredentialIdentities(credentials: credentials) return credentials }, - selectionHandler: { [weak self] credential in + selectionHandler: { [weak self] identifier, password in guard let self = self else { return } let passwordCredential = ASPasswordCredential( - user: credential.username ?? "", - password: credential.password?.value ?? "" + user: identifier, + password: password ) self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) }, @@ -49,13 +49,13 @@ class CredentialProviderViewController: ASCredentialProviderViewController { )) } ) - + self.viewModel = viewModel - + let hostingController = UIHostingController( rootView: CredentialProviderView(viewModel: viewModel) ) - + addChild(hostingController) view.addSubview(hostingController.view) @@ -87,12 +87,14 @@ class CredentialProviderViewController: ASCredentialProviderViewController { override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { do { let credentials = try VaultStore.shared.getAllCredentials() - + if let matchingCredential = credentials.first(where: { credential in return credential.id.uuidString == credentialIdentity.recordIdentifier }) { + // Use the identifier that matches the credential identity + let identifier = credentialIdentity.user let passwordCredential = ASPasswordCredential( - user: matchingCredential.username ?? "", + user: identifier, password: matchingCredential.password?.value ?? "" ) self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) @@ -114,7 +116,7 @@ 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. diff --git a/mobile-app/ios/VaultUI/Views/CredentialProviderView.swift b/mobile-app/ios/VaultUI/Views/CredentialProviderView.swift index dc4e82cf4..2f7109079 100644 --- a/mobile-app/ios/VaultUI/Views/CredentialProviderView.swift +++ b/mobile-app/ios/VaultUI/Views/CredentialProviderView.swift @@ -106,6 +106,33 @@ public struct CredentialProviderView: View { .sheet(isPresented: $viewModel.showAddCredential) { AddCredentialView(viewModel: viewModel) } + .actionSheet(isPresented: $viewModel.showSelectionOptions) { + guard let credential = viewModel.selectedCredential else { + return ActionSheet(title: Text("Select Login Method"), message: Text("No credential selected."), buttons: [.cancel()]) + } + + var buttons: [ActionSheet.Button] = [] + + if let username = credential.username, !username.isEmpty { + buttons.append(.default(Text("Username: \(username)")) { + viewModel.selectUsernamePassword() + }) + } + + if let email = credential.alias?.email, !email.isEmpty { + buttons.append(.default(Text("Email: \(email)")) { + viewModel.selectEmailPassword() + }) + } + + buttons.append(.cancel()) + + return ActionSheet( + title: Text("Select Login Method"), + message: Text("Choose how you want to log in"), + buttons: buttons + ) + } .alert("Error", isPresented: $viewModel.showError) { Button("OK") { viewModel.dismissError() @@ -134,18 +161,20 @@ public class CredentialProviderViewModel: ObservableObject { @Published var showError = false @Published var errorMessage = "" @Published var showAddCredential = false + @Published var showSelectionOptions = false + @Published var selectedCredential: Credential? @Published var newUsername = "" @Published var newPassword = "" @Published var newService = "" private let loader: () async throws -> [Credential] - private let selectionHandler: (Credential) -> Void + private let selectionHandler: (String, String) -> Void private let cancelHandler: () -> Void public init( loader: @escaping () async throws -> [Credential], - selectionHandler: @escaping (Credential) -> Void, + selectionHandler: @escaping (String, String) -> Void, cancelHandler: @escaping () -> Void ) { self.loader = loader @@ -245,7 +274,32 @@ public class CredentialProviderViewModel: ObservableObject { } func selectCredential(_ credential: Credential) { - selectionHandler(credential) + selectedCredential = credential + + // If we only have one option, use it directly + let username = credential.username?.trimmingCharacters(in: .whitespacesAndNewlines) + let email = credential.alias?.email?.trimmingCharacters(in: .whitespacesAndNewlines) + + if (username?.isEmpty ?? true) || (email?.isEmpty ?? true) { + let identifier = username?.isEmpty == false ? username! : (email ?? "") + selectionHandler(identifier, credential.password?.value ?? "") + return + } + + // If we have both options, show selection sheet + showSelectionOptions = true + } + + func selectUsernamePassword() { + guard let credential = selectedCredential else { return } + selectionHandler(credential.username ?? "", credential.password?.value ?? "") + showSelectionOptions = false + } + + func selectEmailPassword() { + guard let credential = selectedCredential else { return } + selectionHandler(credential.alias?.email ?? "", credential.password?.value ?? "") + showSelectionOptions = false } func cancel() { @@ -464,8 +518,8 @@ class PreviewCredentialProviderViewModel: CredentialProviderViewModel { try? await Task.sleep(nanoseconds: 1_000_000_000) // Simulate network delay return previewCredentials }, - selectionHandler: { credential in - print("Selected credential: \(credential)") + selectionHandler: { identifier, password in + print("Selected credential: \(identifier) with password: \(password)") }, cancelHandler: { print("Canceled")