Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Credential provider QuickType support #3696

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ final class CredentialProviderListViewController: UIViewController {
searchController.searchResultsUpdater = self
searchController.searchBar.delegate = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = UserText.autofillLoginListSearchPlaceholder
searchController.searchBar.placeholder = UserText.credentialProviderListSearchPlaceholder
navigationItem.hidesSearchBarWhenScrolling = false
definesPresentationContext = true

Expand Down Expand Up @@ -83,9 +83,12 @@ final class CredentialProviderListViewController: UIViewController {

init(serviceIdentifiers: [ASCredentialServiceIdentifier],
secureVault: (any AutofillSecureVault)?,
credentialIdentityStoreManager: AutofillCredentialIdentityStoreManaging,
onRowSelected: @escaping (AutofillLoginItem) -> Void,
onDismiss: @escaping () -> Void) {
self.viewModel = CredentialProviderListViewModel(serviceIdentifiers: serviceIdentifiers, secureVault: secureVault)
self.viewModel = CredentialProviderListViewModel(serviceIdentifiers: serviceIdentifiers,
secureVault: secureVault,
credentialIdentityStoreManager: credentialIdentityStoreManager)
self.onRowSelected = onRowSelected
self.onDismiss = onDismiss

Expand All @@ -101,7 +104,7 @@ final class CredentialProviderListViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

title = UserText.autofillLoginListTitle
title = UserText.credentialProviderListTitle

let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
navigationItem.rightBarButtonItem = doneItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ final class CredentialProviderListViewModel: ObservableObject {

private let serviceIdentifiers: [ASCredentialServiceIdentifier]
private let secureVault: (any AutofillSecureVault)?
private let credentialIdentityStoreManager: AutofillCredentialIdentityStoreManaging
private var accounts = [SecureVaultModels.WebsiteAccount]()
private var accountsToSuggest = [SecureVaultModels.WebsiteAccount]()
private var cancellables: Set<AnyCancellable> = []
private let tld: TLD = TLD()
private let autofillDomainNameUrlMatcher = AutofillDomainNameUrlMatcher()
private let autofillDomainNameUrlSort = AutofillDomainNameUrlSort()

let authenticator = UserAuthenticator(reason: UserText.autofillLoginListAuthenticationReason,
cancelTitle: UserText.autofillLoginListAuthenticationCancelButton)
let authenticator = UserAuthenticator(reason: UserText.credentialProviderListAuthenticationReason,
cancelTitle: UserText.credentialProviderListAuthenticationCancelButton)
var hasAccountsSaved: Bool {
return !accounts.isEmpty
}
Expand All @@ -62,9 +63,11 @@ final class CredentialProviderListViewModel: ObservableObject {
}

init(serviceIdentifiers: [ASCredentialServiceIdentifier],
secureVault: (any AutofillSecureVault)?) {
secureVault: (any AutofillSecureVault)?,
credentialIdentityStoreManager: AutofillCredentialIdentityStoreManaging) {
self.serviceIdentifiers = serviceIdentifiers
self.secureVault = secureVault
self.credentialIdentityStoreManager = credentialIdentityStoreManager

if let count = getAccountsCount() {
authenticationNotRequired = count == 0
Expand Down Expand Up @@ -103,16 +106,10 @@ final class CredentialProviderListViewModel: ObservableObject {
self.accounts = fetchAccounts()
self.accountsToSuggest = fetchSuggestedAccounts()
self.sections = makeSections(with: accounts)
}

func updateLastUsed(for account: SecureVaultModels.WebsiteAccount) {
guard let secureVault = secureVault,
let accountID = account.id,
let accountIdInt = Int64(accountID) else {
return
Task {
await credentialIdentityStoreManager.replaceCredentialStore(with: accounts)
}

try? secureVault.updateLastUsedFor(accountId: accountIdInt)
}

private func setupCancellables() {
Expand Down Expand Up @@ -200,13 +197,13 @@ final class CredentialProviderListViewModel: ObservableObject {
autofillDomainNameUrlMatcher: autofillDomainNameUrlMatcher,
autofillDomainNameUrlSort: autofillDomainNameUrlSort)
}
newSections.append(.suggestions(title: UserText.autofillLoginListSuggested, items: accountItems))
newSections.append(.suggestions(title: UserText.credentialProviderListSuggested, items: accountItems))
}

let viewModelsGroupedByFirstLetter = accounts.groupedByFirstLetter(
tld: tld,
autofillDomainNameUrlMatcher: autofillDomainNameUrlMatcher,
autofillDomainNameUrlSort: autofillDomainNameUrlSort)
tld: tld,
autofillDomainNameUrlMatcher: autofillDomainNameUrlMatcher,
autofillDomainNameUrlSort: autofillDomainNameUrlSort)
let accountSections = viewModelsGroupedByFirstLetter.sortedIntoSections(autofillDomainNameUrlSort,
tld: tld)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ final class EmptySearchView: UIView {
let label = UILabel(frame: CGRect.zero)

label.font = .systemFont(ofSize: UIFont.preferredFont(forTextStyle: .title2).pointSize * 1.091, weight: .regular)
label.text = UserText.autofillSearchNoResultTitle
label.text = UserText.credentialProviderListSearchNoResultTitle
label.numberOfLines = 0
label.textAlignment = .center
label.lineBreakMode = .byWordWrapping
Expand Down Expand Up @@ -57,7 +57,7 @@ final class EmptySearchView: UIView {
var query: String = "" {
didSet {
if query.count > 0 {
subtitle.text = UserText.autofillSearchNoResultSubtitle(for: query)
subtitle.text = UserText.credentialProviderListSearchNoResultSubtitle(for: query)
} else {
subtitle.text = ""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ struct EmptyView: View {
.aspectRatio(contentMode: .fit)
.frame(width: 96, height: 96)

Text(UserText.autofillEmptyViewTitle)
Text(UserText.credentialProviderListEmptyViewTitle)
.daxTitle3()
.foregroundColor(Color(designSystemColor: .textPrimary))
.padding(.top, 16)
.multilineTextAlignment(.center)
.lineLimit(nil)

Text(UserText.autofillEmptyViewSubtitle)
Text(UserText.credentialProviderListEmptyViewSubtitle)
.daxBodyRegular()
.foregroundColor(Color.init(designSystemColor: .textSecondary))
.multilineTextAlignment(.center)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,56 +21,121 @@ import AuthenticationServices
import SwiftUI
import BrowserServicesKit
import Core
import Common

class CredentialProviderViewController: ASCredentialProviderViewController {

final class CredentialProviderViewController: ASCredentialProviderViewController {

private struct Constants {
static let openPasswords = AppDeepLinkSchemes.openPasswords.url
}


private lazy var authenticator = UserAuthenticator(reason: UserText.credentialProviderListAuthenticationReason,
cancelTitle: UserText.credentialProviderListAuthenticationCancelButton)

private lazy var credentialIdentityStoreManager: AutofillCredentialIdentityStoreManaging = AutofillCredentialIdentityStoreManager(credentialStore: ASCredentialIdentityStore.shared,
vault: secureVault,
tld: tld)

private lazy var secureVault: (any AutofillSecureVault)? = {
return try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter())
try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter())
}()

private lazy var passwordFetcher: CredentialFetcher = {
return CredentialFetcher(secureVault: secureVault)
}()


private lazy var tld: TLD = TLD()

private lazy var vaultCredentialManager: VaultCredentialManaging = VaultCredentialManager(secureVault: secureVault,
credentialIdentityStoreManager: credentialIdentityStoreManager)

// MARK: - ASCredentialProviderViewController Overrides

override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
loadCredentialsList(for: serviceIdentifiers)
}

override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
// A quirk here is calling .canAuthenticate in this one scenario actually triggers the prompt to authentication
// Calling .authenticate here results in the extension attempting to present a non-existent view controller causing weird UI
if authenticator.canAuthenticateViaBiometrics() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If biometrics authentication is not possible (or no device auth is set), userInteractionRequired is returned here. This triggers a call to prepareInterfaceToProvideCredential(for credentialRequest: any ASCredentialRequest) which presents a lock screen UI and complete authentication & error handling

provideCredential(for: credentialIdentity)
} else {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain,
code: ASExtensionError.userInteractionRequired.rawValue))
}
}

@available(iOS 17.0, *)
override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) {
guard credentialRequest.type == .password else {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain,
code: ASExtensionError.credentialIdentityNotFound.rawValue))
return
}

if authenticator.canAuthenticateViaBiometrics() {
provideCredential(for: credentialRequest.credentialIdentity)
} else {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain,
code: ASExtensionError.userInteractionRequired.rawValue))
}
}

override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
let hostingController = UIHostingController(rootView: LockScreenView())
installChildViewController(hostingController)

authenticateAndHandleCredential {
self.provideCredential(for: credentialIdentity)
}
}

@available(iOS 17.0, *)
override func prepareInterfaceToProvideCredential(for credentialRequest: any ASCredentialRequest) {
let hostingController = UIHostingController(rootView: LockScreenView())
installChildViewController(hostingController)

authenticateAndHandleCredential {
self.provideCredential(for: credentialRequest.credentialIdentity)
}
}

override func prepareInterfaceForExtensionConfiguration() {
let viewModel = CredentialProviderActivatedViewModel { [weak self] shouldLaunchApp in
if shouldLaunchApp {
self?.openUrl(Constants.openPasswords)
}
self?.extensionContext.completeExtensionConfigurationRequest()
}

let view = CredentialProviderActivatedView(viewModel: viewModel)
let hostingController = UIHostingController(rootView: view)
installChildViewController(hostingController)

Task {
await credentialIdentityStoreManager.populateCredentialStore()
}
}

override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
loadCredentialsList(for: serviceIdentifiers)
}


// MARK: - Private

private func loadCredentialsList(for serviceIdentifiers: [ASCredentialServiceIdentifier], returnString: Bool = false) {
let credentialProviderListViewController = CredentialProviderListViewController(serviceIdentifiers: serviceIdentifiers,
secureVault: secureVault,
credentialIdentityStoreManager: credentialIdentityStoreManager,
onRowSelected: { [weak self] item in
guard let self = self else {
self?.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain,
code: ASExtensionError.failed.rawValue))
return
}

let credential = self.passwordFetcher.fetchCredential(for: item.account)
self.extensionContext.completeRequest(withSelectedCredential: credential, completionHandler: nil)

}, onDismiss: {
guard let self = self else {
self?.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain,
code: ASExtensionError.failed.rawValue))
return
}

let credential = self.vaultCredentialManager.fetchCredential(for: item.account)

self.extensionContext.completeRequest(withSelectedCredential: credential, completionHandler: nil)

}, onDismiss: {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain,
code: ASExtensionError.userCanceled.rawValue))
})

let navigationController = UINavigationController(rootViewController: credentialProviderListViewController)
self.view.subviews.forEach { $0.removeFromSuperview() }
addChild(navigationController)
Expand All @@ -79,5 +144,46 @@ final class CredentialProviderViewController: ASCredentialProviderViewController
self.view.addSubview(navigationController.view)
navigationController.didMove(toParent: self)
}


@available(iOS 17.0, *)
private func provideCredential(for credentialIdentity: ASCredentialIdentity) {
guard let passwordCredential = vaultCredentialManager.fetchCredential(for: credentialIdentity) else {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain,
code: ASExtensionError.credentialIdentityNotFound.rawValue))
return
}

self.extensionContext.completeRequest(withSelectedCredential: passwordCredential)
}

private func provideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
guard let passwordCredential = vaultCredentialManager.fetchCredential(for: credentialIdentity) else {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain,
code: ASExtensionError.credentialIdentityNotFound.rawValue))
return
}

self.extensionContext.completeRequest(withSelectedCredential: passwordCredential)
}

private func authenticateAndHandleCredential(provideCredential: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.authenticator.authenticate { error in
if error != nil {
if error != .noAuthAvailable {
self?.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain,
code: ASExtensionError.userInteractionRequired.rawValue))
} else {
let alert = UIAlertController.makeDeviceAuthenticationAlert { [weak self] in
self?.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain,
code: ASExtensionError.userInteractionRequired.rawValue))
}
self?.present(alert, animated: true)
}
} else {
provideCredential()
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ extension UIAlertController {
}

let alertController = UIAlertController(
title: UserText.autofillNoDeviceAuthSetTitle,
message: String(format: UserText.autofillNoDeviceAuthSetMessage, deviceType),
title: UserText.credentialProviderNoDeviceAuthSetTitle,
message: String(format: UserText.credentialProviderNoDeviceAuthSetMessage, deviceType),
preferredStyle: .alert
)

Expand Down
Loading
Loading