Add "text to insert" method support (#771)

This commit is contained in:
Leendert de Borst
2025-04-28 16:39:18 +02:00
parent a37052e4dc
commit 27dbd53786
2 changed files with 124 additions and 65 deletions

View File

@@ -21,6 +21,7 @@ import VaultModels
*/
class CredentialProviderViewController: ASCredentialProviderViewController {
private var viewModel: CredentialProviderViewModel?
private var isChoosingTextToInsert = false
override func viewDidLoad() {
super.viewDidLoad()
@@ -35,11 +36,25 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
},
selectionHandler: { [weak self] identifier, password in
guard let self = self else { return }
let passwordCredential = ASPasswordCredential(
user: identifier,
password: password
)
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
if self.isChoosingTextToInsert {
// For text insertion, insert only the selected text
if #available(iOS 18.0, *) {
self.extensionContext.completeRequest(
withTextToInsert: identifier,
completionHandler: nil
)
} else {
// Fallback on earlier versions: do nothing as this feature
// is not supported and we should not reach this point?
}
} else {
// For regular credential selection
let passwordCredential = ASPasswordCredential(
user: identifier,
password: password
)
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
}
},
cancelHandler: { [weak self] in
guard let self = self else { return }
@@ -81,7 +96,8 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
}
override func prepareInterfaceForUserChoosingTextToInsert() {
// This is handled in the SwiftUI view's onAppear
isChoosingTextToInsert = true
viewModel?.isChoosingTextToInsert = true
}
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {

View File

@@ -48,22 +48,24 @@ public struct CredentialProviderView: View {
.foregroundColor(colorScheme == .dark ? ColorConstants.Dark.text : ColorConstants.Light.text)
.multilineTextAlignment(.center)
VStack(spacing: 12) {
Button(action: {
viewModel.showAddCredential = true
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Create New Credential")
if !viewModel.isChoosingTextToInsert {
VStack(spacing: 12) {
Button(action: {
viewModel.showAddCredential = true
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Create New Credential")
}
.padding()
.frame(maxWidth: .infinity)
.background(ColorConstants.Light.primary)
.foregroundColor(.white)
.cornerRadius(8)
}
.padding()
.frame(maxWidth: .infinity)
.background(ColorConstants.Light.primary)
.foregroundColor(.white)
.cornerRadius(8)
}
.padding(.horizontal, 40)
}
.padding(.horizontal, 40)
}
.padding(.top, 60)
} else {
@@ -84,7 +86,7 @@ public struct CredentialProviderView: View {
}
}
}
.navigationTitle("Select Credential")
.navigationTitle(viewModel.isChoosingTextToInsert ? "Select Text to Insert" : "Select Credential")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
@@ -93,7 +95,6 @@ public struct CredentialProviderView: View {
}
.foregroundColor(ColorConstants.Light.primary)
}
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
Button("Add") {
@@ -107,29 +108,51 @@ public struct CredentialProviderView: View {
AddCredentialView(viewModel: viewModel)
}
.actionSheet(isPresented: $viewModel.showSelectionOptions) {
// Define all text strings
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 viewModel.isChoosingTextToInsert {
if let username = credential.username, !username.isEmpty {
buttons.append(.default(Text("Username: \(username)")) {
viewModel.selectUsername()
})
}
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.selectEmail()
})
}
buttons.append(.default(Text("Password")) {
viewModel.selectPassword()
})
}
else {
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()
})
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"),
title: viewModel.isChoosingTextToInsert ? Text("Select Text To Insert") : Text("Select Login Method"),
message: viewModel.isChoosingTextToInsert ? Text("Select the text to insert into the focused input field") : Text("Choose how you want to log in"),
buttons: buttons
)
}
@@ -163,6 +186,7 @@ public class CredentialProviderViewModel: ObservableObject {
@Published var showAddCredential = false
@Published var showSelectionOptions = false
@Published var selectedCredential: Credential?
@Published public var isChoosingTextToInsert = false
@Published var newUsername = ""
@Published var newPassword = ""
@@ -276,6 +300,12 @@ public class CredentialProviderViewModel: ObservableObject {
func selectCredential(_ credential: Credential) {
selectedCredential = credential
// If we're in text insertion mode, always show the selection sheet
if isChoosingTextToInsert {
showSelectionOptions = true
return
}
// If we only have one option, use it directly
let username = credential.username?.trimmingCharacters(in: .whitespacesAndNewlines)
let email = credential.alias?.email?.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -289,6 +319,24 @@ public class CredentialProviderViewModel: ObservableObject {
// If we have both options, show selection sheet
showSelectionOptions = true
}
func selectUsername() {
guard let credential = selectedCredential else { return }
selectionHandler(credential.username ?? "", "")
showSelectionOptions = false
}
func selectEmail() {
guard let credential = selectedCredential else { return }
selectionHandler(credential.alias?.email ?? "", "")
showSelectionOptions = false
}
func selectPassword() {
guard let credential = selectedCredential else { return }
selectionHandler(credential.password?.value ?? "", "")
showSelectionOptions = false
}
func selectUsernamePassword() {
guard let credential = selectedCredential else { return }
@@ -464,12 +512,9 @@ extension Credential {
}
}
// Preview setup
class PreviewCredentialProviderViewModel: CredentialProviderViewModel {
@Published var showSelectionAlert = false
@Published var selectedCredentialInfo = ""
@Published var showCancelAlert = false
init() {
let previewCredentials = [
.preview,
@@ -491,50 +536,48 @@ 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
)
]
super.init(
loader: {
try? await Task.sleep(nanoseconds: 1_000_000_000) // Simulate network delay
try? await Task.sleep(nanoseconds: 1_000_000_000)
return previewCredentials
},
selectionHandler: { identifier, password in
print("Selected credential: \(identifier) with password: \(password)")
},
cancelHandler: {
print("Canceled")
}
selectionHandler: { _, _ in },
cancelHandler: {}
)
self.credentials = previewCredentials
self.filteredCredentials = previewCredentials
self.isLoading = false
credentials = previewCredentials
filteredCredentials = previewCredentials
isLoading = false
}
}
struct CredentialProviderView_Previews: PreviewProvider {
static func makePreview(isChoosing: Bool, showingSelection: Bool, colorScheme: ColorScheme) -> some View {
let vm = PreviewCredentialProviderViewModel()
vm.isChoosingTextToInsert = isChoosing
if showingSelection {
vm.selectedCredential = .preview
vm.showSelectionOptions = true
}
return CredentialProviderView(viewModel: vm)
.environment(\.colorScheme, colorScheme)
}
static var previews: some View {
let viewModel = PreviewCredentialProviderViewModel()
CredentialProviderView(viewModel: viewModel)
Group {
makePreview(isChoosing: false, showingSelection: false, colorScheme: .light)
.previewDisplayName("Light - Normal")
makePreview(isChoosing: false, showingSelection: false, colorScheme: .dark)
.previewDisplayName("Dark - Normal")
makePreview(isChoosing: true, showingSelection: false, colorScheme: .light)
.previewDisplayName("Light - Insert Text Mode")
makePreview(isChoosing: true, showingSelection: true, colorScheme: .light)
.previewDisplayName("Light - Insert Text Mode Selection")
makePreview(isChoosing: false, showingSelection: true, colorScheme: .light)
.previewDisplayName("Light - Selection Sheet")
}
}
}