Add actionsheet when selecting autofill credential to choose username or email (#771)

This commit is contained in:
Leendert de Borst
2025-04-28 16:07:19 +02:00
parent cd8b2deb04
commit a37052e4dc
2 changed files with 70 additions and 14 deletions

View File

@@ -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.

View File

@@ -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")