Files
aliasvault/apps/mobile-app/ios/Autofill/CredentialProviderViewController+Credential.swift
2026-05-12 20:15:32 +02:00

178 lines
7.2 KiB
Swift

import AuthenticationServices
import SwiftUI
import VaultStoreKit
import VaultUI
import VaultModels
import VaultUtils
/**
* Credential-specific functionality for CredentialProviderViewController
* This extension handles all password credential operations
*/
extension CredentialProviderViewController: CredentialProviderDelegate {
// MARK: - CredentialProviderDelegate Implementation
func setupCredentialView(vaultStore: VaultStore, serviceUrl: String?) throws -> UIViewController {
// Create the ViewModel with injected behaviors
let viewModel = CredentialProviderViewModel(
loader: {
return try await vaultStore.getAllAutofillCredentials()
},
selectionHandler: { identifier, password in
self.handleCredentialSelection(identifier: identifier, password: password)
},
cancelHandler: {
self.handleCancel()
},
serviceUrl: serviceUrl,
urlLinker: { itemId, url in
/*
* Step 1 Append the URL/app identifier to the chosen credential's
* `login.url` multi-value field.
*/
do {
try vaultStore.appendUrl(toItemId: itemId, url: url)
} catch {
print("[Autofill] Failed to append URL to credential: \(error)")
return
}
/*
* Step 2 Push the change to the server (skipped if offline, client will retry later).
*/
let webApiService = WebApiService()
do {
try await vaultStore.mutateVault(using: webApiService)
} catch {
print("[Autofill] Vault sync after URL link failed (vault stays dirty for main app to retry): \(error)")
}
/*
* Step 3 Refresh the iOS credential identity store with the
* new URL.
*/
do {
let credentials = try vaultStore.getAllAutofillCredentials()
try await CredentialIdentityStore.shared.saveCredentialIdentities(credentials)
print("[Autofill] Refreshed iOS credential identity cache (\(credentials.count) credentials)")
} catch {
print("[Autofill] Failed to refresh iOS credential identity cache: \(error)")
}
}
)
// Set text insertion mode if needed
if isChoosingTextToInsert {
viewModel.isChoosingTextToInsert = true
}
let hostingController = UIHostingController(
rootView: CredentialProviderView(viewModel: viewModel)
)
return hostingController
}
func handleCredentialSelection(identifier: String, password: String) {
if 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)
}
}
// MARK: - Credential-specific Methods
override public func prepareInterfaceForUserChoosingTextToInsert() {
isChoosingTextToInsert = true
// This will be handled by the credential view model when it's created
}
/**
* Handle quick return password credential request
* Called from viewWillAppear when in quick return mode with vault already unlocked
* Ensures minimum 700ms duration for smooth UX (prevents flash/jitter)
*/
internal func handleQuickReturnPasswordCredential(vaultStore: VaultStore, request: ASPasswordCredentialRequest) {
// Track start time for minimum duration
let startTime = Date()
let minimumDuration: TimeInterval = 0.7 // 700ms
do {
let credentials = try vaultStore.getAllAutofillCredentials()
if let matchingCredential = credentials.first(where: { credential in
return credential.id.uuidString == request.credentialIdentity.recordIdentifier
}) {
// Ensure minimum duration before completing
let elapsed = Date().timeIntervalSince(startTime)
if elapsed < minimumDuration {
Thread.sleep(forTimeInterval: minimumDuration - elapsed)
}
// If the credential has a TOTP secret and the user has the
// copy-on-fill setting enabled (default), put the current
// TOTP code on the clipboard so they can paste it into the
// 2FA field after the autofill completes.
if matchingCredential.hasTotp,
let secret = matchingCredential.totpSecret,
AutofillSettings.shouldCopyTotpOnFill,
let code = TotpGenerator.generateCode(secret: secret),
!code.isEmpty {
UIPasteboard.general.string = code
}
// Use the identifier that matches the credential identity
let identifier = request.credentialIdentity.user
let passwordCredential = ASPasswordCredential(
user: identifier,
password: matchingCredential.password ?? ""
)
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
} else {
// Ensure minimum duration even on error
let elapsed = Date().timeIntervalSince(startTime)
if elapsed < minimumDuration {
Thread.sleep(forTimeInterval: minimumDuration - elapsed)
}
self.extensionContext.cancelRequest(
withError: NSError(
domain: ASExtensionErrorDomain,
code: ASExtensionError.credentialIdentityNotFound.rawValue
)
)
}
} catch {
// Ensure minimum duration even on error
let elapsed = Date().timeIntervalSince(startTime)
if elapsed < minimumDuration {
Thread.sleep(forTimeInterval: minimumDuration - elapsed)
}
print("handleQuickReturnPasswordCredential error: \(error)")
self.extensionContext.cancelRequest(
withError: NSError(
domain: ASExtensionErrorDomain,
code: ASExtensionError.failed.rawValue,
userInfo: [NSLocalizedDescriptionKey: error.localizedDescription]
)
)
}
}
}