From 4881936004291d3f964543a713ed96fbaa2775a3 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Mon, 9 Dec 2024 15:42:22 +0100 Subject: [PATCH] Credential provider QuickType support (#3696) **Description**: Adds support for QuickType to the Credential provider extension (along with deduplication logic for the QuickType prompts) --- ...CredentialProviderListViewController.swift | 9 +- .../CredentialProviderListViewModel.swift | 27 ++- .../EmptySearchView.swift | 4 +- .../CredentialProviderList/EmptyView.swift | 4 +- .../CredentialProviderViewController.swift | 160 +++++++++++++++--- .../UIAlertControllerExtension.swift | 4 +- .../Resources/UserText.swift | 30 ++-- .../Shared/AutofillPasswordFetcher.swift | 52 ------ .../Shared/VaultCredentialManager.swift | 111 ++++++++++++ Core/SyncCredentialsAdapter.swift | 14 +- Core/SyncDataProviders.swift | 6 +- Core/UserAuthenticator.swift | 6 + DuckDuckGo.xcodeproj/project.pbxproj | 16 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- DuckDuckGo/AppDelegate.swift | 3 +- DuckDuckGo/AutofillLoginListViewModel.swift | 10 +- DuckDuckGo/TabViewController.swift | 23 ++- .../OnboardingDaxFavouritesTests.swift | 4 +- .../OnboardingNavigationDelegateTests.swift | 4 +- .../SyncCredentialsAdapterTests.swift | 3 +- ...SyncSettingsViewControllerErrorTests.swift | 4 +- alphaAdhocExportOptions.plist | 2 + fastlane/Fastfile | 8 + fastlane/Matchfile | 4 +- 24 files changed, 369 insertions(+), 141 deletions(-) delete mode 100644 AutofillCredentialProvider/CredentialProvider/Shared/AutofillPasswordFetcher.swift create mode 100644 AutofillCredentialProvider/CredentialProvider/Shared/VaultCredentialManager.swift diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/CredentialProviderListViewController.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/CredentialProviderListViewController.swift index 72c13ae9ee..e0d0e8eaff 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/CredentialProviderListViewController.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/CredentialProviderListViewController.swift @@ -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 @@ -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 @@ -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 diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/CredentialProviderListViewModel.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/CredentialProviderListViewModel.swift index 2341846737..40664022a6 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/CredentialProviderListViewModel.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/CredentialProviderListViewModel.swift @@ -41,6 +41,7 @@ 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 = [] @@ -48,8 +49,8 @@ final class CredentialProviderListViewModel: ObservableObject { 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 } @@ -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 @@ -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() { @@ -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) diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/EmptySearchView.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/EmptySearchView.swift index 5f0be33f2c..b61c976612 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/EmptySearchView.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/EmptySearchView.swift @@ -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 @@ -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 = "" } diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/EmptyView.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/EmptyView.swift index 53bf6c0e9b..7cce2d7329 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/EmptyView.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderList/EmptyView.swift @@ -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) diff --git a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift index c41709f917..397b1a5781 100644 --- a/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift +++ b/AutofillCredentialProvider/CredentialProvider/CredentialProviderViewController.swift @@ -21,21 +21,82 @@ 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() { + 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 { @@ -43,34 +104,38 @@ final class CredentialProviderViewController: ASCredentialProviderViewController } 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) @@ -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() + } + } + } + } } diff --git a/AutofillCredentialProvider/CredentialProvider/Extensions/UIAlertControllerExtension.swift b/AutofillCredentialProvider/CredentialProvider/Extensions/UIAlertControllerExtension.swift index 96a412197d..2908e6e5d9 100644 --- a/AutofillCredentialProvider/CredentialProvider/Extensions/UIAlertControllerExtension.swift +++ b/AutofillCredentialProvider/CredentialProvider/Extensions/UIAlertControllerExtension.swift @@ -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 ) diff --git a/AutofillCredentialProvider/CredentialProvider/Resources/UserText.swift b/AutofillCredentialProvider/CredentialProvider/Resources/UserText.swift index 493be11598..74922adc7a 100644 --- a/AutofillCredentialProvider/CredentialProvider/Resources/UserText.swift +++ b/AutofillCredentialProvider/CredentialProvider/Resources/UserText.swift @@ -31,33 +31,33 @@ final class UserText { static let actionClose = NSLocalizedString("action.button.cancel", value: "Close", comment: "Close button title") - static let autofillLoginListTitle = NSLocalizedString("autofill.logins.list.title", value: "Passwords", comment: "Title for screen listing autofill logins") + static let credentialProviderListTitle = NSLocalizedString("credential.provider.list.title", value: "Passwords", comment: "Title for screen listing autofill logins") - static let autofillLoginListSearchPlaceholder = NSLocalizedString("autofill.logins.list.search-placeholder", value: "Search passwords", comment: "Placeholder for search field on autofill login listing") + static let credentialProviderListSearchPlaceholder = NSLocalizedString("credential.provider.list.search-placeholder", value: "Search passwords", comment: "Placeholder for search field on autofill login listing") - static let autofillEmptyViewTitle = NSLocalizedString("autofill.logins.empty-view.title", value: "No passwords saved yet", comment: "Title for view displayed when autofill has no items") + static let credentialProviderListEmptyViewTitle = NSLocalizedString("credential.provider.list.empty-view.title", value: "No passwords saved yet", comment: "Title for view displayed when autofill has no items") - static let autofillEmptyViewSubtitle = NSLocalizedString("autofill.logins.list.enable.footer", value: "Passwords are stored securely on your device.", comment: "Footer label displayed below table section with option to enable autofill") + static let credentialProviderListEmptyViewSubtitle = NSLocalizedString("credential.provider.list.empty-view.footer", value: "Passwords are stored securely on your device.", comment: "Footer label displayed below table section with option to enable autofill") - static let autofillLoginListSuggested = NSLocalizedString("autofill.logins.list.suggested", value: "Suggested", comment: "Section title for group of suggested saved logins") + static let credentialProviderListSuggested = NSLocalizedString("credential.provider.list.suggested", value: "Suggested", comment: "Section title for group of suggested saved logins") - static let autofillSearchNoResultTitle = NSLocalizedString("autofill.logins.search.no-results.title", value: "No Results", comment: "Title displayed when there are no results on Autofill search") + static let credentialProviderListSearchNoResultTitle = NSLocalizedString("credential.provider.list.search.no-results.title", value: "No Results", comment: "Title displayed when there are no results on Autofill search") - static func autofillSearchNoResultSubtitle(for query: String) -> String { - let message = NSLocalizedString("autofill.logins.search.no-results.subtitle", value: "for '%@'", comment: "Subtitle displayed when there are no results on Autofill search, example : No Result (Title) for Duck (Subtitle)") + static func credentialProviderListSearchNoResultSubtitle(for query: String) -> String { + let message = NSLocalizedString("credential.provider.list.search.no-results.subtitle", value: "for '%@'", comment: "Subtitle displayed when there are no results on Autofill search, example : No Result (Title) for Duck (Subtitle)") return message.format(arguments: query) } - static let autofillLoginListAuthenticationReason = NSLocalizedString("autofill.logins.list.auth.reason", value: "Unlock device to access passwords", comment: "Reason for auth when opening login list") + static let credentialProviderListAuthenticationReason = NSLocalizedString("credential.provider.list.auth.reason", value: "Unlock device to access passwords", comment: "Reason for auth when opening screen with list of saved passwords") - static let autofillLoginListAuthenticationCancelButton = NSLocalizedString("autofill.logins.list.auth.cancel", value: "Cancel", comment: "Cancel button for auth when opening login list") + static let credentialProviderListAuthenticationCancelButton = NSLocalizedString("credential.provider.list.auth.cancel", value: "Cancel", comment: "Cancel button for auth when opening login list") - static let autofillNoDeviceAuthSetTitle = NSLocalizedString("autofill.no-device-auth-set.title", value: "Device Passcode Required", comment: "Title for alert when device authentication is not set") + static let credentialProviderNoDeviceAuthSetTitle = NSLocalizedString("credential.provider.no-device-auth-set.title", value: "Device Passcode Required", comment: "Title for alert when device authentication is not set") - static let autofillNoDeviceAuthSetMessage = NSLocalizedString("autofill.no-device-auth-set.message", value: "Set a passcode on %@ to autofill your DuckDuckGo passwords.", comment: "Message for alert when device authentication is not set, where %@ is iPhone|iPad|device") + static let credentialProviderNoDeviceAuthSetMessage = NSLocalizedString("credential.provider.no-device-auth-set.message", value: "Set a passcode on %@ to autofill your DuckDuckGo passwords.", comment: "Message for alert when device authentication is not set, where %@ is iPhone|iPad|device") - static let deviceTypeiPhone = NSLocalizedString("device.type.iphone", value: "iPhone", comment: "Device type is iPhone") - static let deviceTypeiPad = NSLocalizedString("device.type.pad", value: "iPad", comment: "Device type is iPad") - static let deviceTypeDefault = NSLocalizedString("device.type.default", value: "device", comment: "Default string used if users device is not iPhone or iPad") + static let deviceTypeiPhone = NSLocalizedString("credential.provider.device.type.iphone", value: "iPhone", comment: "Device type is iPhone") + static let deviceTypeiPad = NSLocalizedString("credential.provider.device.type.pad", value: "iPad", comment: "Device type is iPad") + static let deviceTypeDefault = NSLocalizedString("credential.providerdevice.type.default", value: "device", comment: "Default string used if users device is not iPhone or iPad") } diff --git a/AutofillCredentialProvider/CredentialProvider/Shared/AutofillPasswordFetcher.swift b/AutofillCredentialProvider/CredentialProvider/Shared/AutofillPasswordFetcher.swift deleted file mode 100644 index c3edbf2c3f..0000000000 --- a/AutofillCredentialProvider/CredentialProvider/Shared/AutofillPasswordFetcher.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// AutofillPasswordFetcher.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import AuthenticationServices -import BrowserServicesKit -import Core -import SecureStorage - -class CredentialFetcher { - - private let secureVault: (any AutofillSecureVault)? - - init(secureVault: (any AutofillSecureVault)?) { - self.secureVault = secureVault - } - - func fetchCredential(for account: SecureVaultModels.WebsiteAccount) -> ASPasswordCredential { - let password = fetchPassword(for: account) - return ASPasswordCredential(user: account.username ?? "", password: password) - } - - private func fetchPassword(for account: SecureVaultModels.WebsiteAccount) -> String { - do { - if let accountID = account.id, let accountIdInt = Int64(accountID), let vault = secureVault { - if let credential = try vault.websiteCredentialsFor(accountId: accountIdInt) { - return credential.password.flatMap { String(data: $0, encoding: .utf8) } ?? "" - } - } - } catch { - Pixel.fire(pixel: .secureVaultError, error: error) - } - - return "" - } -} diff --git a/AutofillCredentialProvider/CredentialProvider/Shared/VaultCredentialManager.swift b/AutofillCredentialProvider/CredentialProvider/Shared/VaultCredentialManager.swift new file mode 100644 index 0000000000..945d58456a --- /dev/null +++ b/AutofillCredentialProvider/CredentialProvider/Shared/VaultCredentialManager.swift @@ -0,0 +1,111 @@ +// +// VaultCredentialManager.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AuthenticationServices +import BrowserServicesKit +import Core +import SecureStorage + +protocol VaultCredentialManaging: AnyObject { + func fetchCredential(for account: SecureVaultModels.WebsiteAccount) -> ASPasswordCredential + func fetchCredential(for identity: ASPasswordCredentialIdentity) -> ASPasswordCredential? + @available(iOS 17.0, *) + func fetchCredential(for identity: ASCredentialIdentity) -> ASPasswordCredential? +} + +final class VaultCredentialManager: VaultCredentialManaging { + + private let secureVault: (any AutofillSecureVault)? + private let credentialIdentityStoreManager: AutofillCredentialIdentityStoreManaging + + init(secureVault: (any AutofillSecureVault)?, + credentialIdentityStoreManager: AutofillCredentialIdentityStoreManaging) { + self.secureVault = secureVault + self.credentialIdentityStoreManager = credentialIdentityStoreManager + } + + func fetchCredential(for account: SecureVaultModels.WebsiteAccount) -> ASPasswordCredential { + let password = retrievePassword(for: account) + + updateLastUsed(for: account) + + return ASPasswordCredential(user: account.username ?? "", password: password) + } + + func fetchCredential(for identity: ASPasswordCredentialIdentity) -> ASPasswordCredential? { + return fetchCredentialHelper(for: identity.user, recordIdentifier: identity.recordIdentifier) + } + + @available(iOS 17.0, *) + func fetchCredential(for identity: ASCredentialIdentity) -> ASPasswordCredential? { + return fetchCredentialHelper(for: identity.user, recordIdentifier: identity.recordIdentifier) + } + + // MARK: - Private + + private func retrievePassword(for account: SecureVaultModels.WebsiteAccount) -> String { + guard let accountID = account.id, let accountIdInt = Int64(accountID), let credentials = retrieveCredentials(for: accountIdInt) else { + return "" + } + + return credentials.password.flatMap { String(data: $0, encoding: .utf8) } ?? "" + } + + private func fetchCredentialHelper(for user: String, recordIdentifier: String?) -> ASPasswordCredential? { + guard let recordIdentifier = recordIdentifier, + let accountIdInt = Int64(recordIdentifier), + let credentials = retrieveCredentials(for: accountIdInt) else { + return nil + } + + let passwordCredential = ASPasswordCredential(user: user, + password: credentials.password.flatMap { String(data: $0, encoding: .utf8) } ?? "") + + updateLastUsed(for: credentials.account) + + return passwordCredential + } + + private func retrieveCredentials(for accountId: Int64) -> SecureVaultModels.WebsiteCredentials? { + guard let vault = secureVault else { return nil } + do { + return try vault.websiteCredentialsFor(accountId: accountId) + } catch { + Pixel.fire(pixel: .secureVaultError, error: error) + return nil + } + } + + private func updateLastUsed(for account: SecureVaultModels.WebsiteAccount) { + if let accountID = account.id, let accountIdInt = Int64(accountID), let vault = secureVault { + do { + try vault.updateLastUsedFor(accountId: accountIdInt) + + Task { + if let domain = account.domain { + await credentialIdentityStoreManager.updateCredentialStore(for: domain) + } + } + } catch { + Pixel.fire(pixel: .secureVaultError, error: error) + } + } + } +} diff --git a/Core/SyncCredentialsAdapter.swift b/Core/SyncCredentialsAdapter.swift index 7fa4b8c509..888341cc87 100644 --- a/Core/SyncCredentialsAdapter.swift +++ b/Core/SyncCredentialsAdapter.swift @@ -33,10 +33,12 @@ public final class SyncCredentialsAdapter { public static let syncCredentialsPausedStateChanged = SyncBookmarksAdapter.syncBookmarksPausedStateChanged public static let credentialsSyncLimitReached = Notification.Name("com.duckduckgo.app.SyncCredentialsLimitReached") let syncErrorHandler: SyncErrorHandling + let credentialIdentityStoreManager: AutofillCredentialIdentityStoreManaging public init(secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory, secureVaultErrorReporter: SecureVaultReporting, - syncErrorHandler: SyncErrorHandling) { + syncErrorHandler: SyncErrorHandling, + tld: TLD) { syncDidCompletePublisher = syncDidCompleteSubject.eraseToAnyPublisher() self.secureVaultErrorReporter = secureVaultErrorReporter self.syncErrorHandler = syncErrorHandler @@ -45,6 +47,8 @@ public final class SyncCredentialsAdapter { secureVaultErrorReporter: secureVaultErrorReporter, errorEvents: CredentialsCleanupErrorHandling() ) + credentialIdentityStoreManager = AutofillCredentialIdentityStoreManager( + vault: try? secureVaultFactory.makeVault(reporter: secureVaultErrorReporter), tld: tld) } public func cleanUpDatabaseAndUpdateSchedule(shouldEnable: Bool) { @@ -74,9 +78,15 @@ public final class SyncCredentialsAdapter { syncDidUpdateData: { [weak self] in self?.syncDidCompleteSubject.send() self?.syncErrorHandler.syncCredentialsSucceded() + }, + syncDidFinish: { [weak self] credentialsInput in + if let credentialsInput, !credentialsInput.modifiedAccounts.isEmpty || !credentialsInput.deletedAccounts.isEmpty { + Task { + await self?.credentialIdentityStoreManager.updateCredentialStoreWith(updatedAccounts: credentialsInput.modifiedAccounts, deletedAccounts: credentialsInput.deletedAccounts) + } + } } ) - syncErrorCancellable = provider.syncErrorPublisher .sink { [weak self] error in self?.syncErrorHandler.handleCredentialError(error) diff --git a/Core/SyncDataProviders.swift b/Core/SyncDataProviders.swift index a32f3b8508..2220b05bd6 100644 --- a/Core/SyncDataProviders.swift +++ b/Core/SyncDataProviders.swift @@ -101,7 +101,8 @@ public class SyncDataProviders: DataProvidersSource { settingHandlers: [SettingSyncHandler], favoritesDisplayModeStorage: FavoritesDisplayModeStoring, syncErrorHandler: SyncErrorHandling, - faviconStoring: FaviconStoring + faviconStoring: FaviconStoring, + tld: TLD ) { self.bookmarksDatabase = bookmarksDatabase self.secureVaultFactory = secureVaultFactory @@ -112,7 +113,8 @@ public class SyncDataProviders: DataProvidersSource { faviconStoring: faviconStoring) credentialsAdapter = SyncCredentialsAdapter(secureVaultFactory: secureVaultFactory, secureVaultErrorReporter: secureVaultErrorReporter, - syncErrorHandler: syncErrorHandler) + syncErrorHandler: syncErrorHandler, + tld: tld) settingsAdapter = SyncSettingsAdapter(settingHandlers: settingHandlers, syncErrorHandler: syncErrorHandler) } diff --git a/Core/UserAuthenticator.swift b/Core/UserAuthenticator.swift index efbeaf277d..551a598b4f 100644 --- a/Core/UserAuthenticator.swift +++ b/Core/UserAuthenticator.swift @@ -56,6 +56,12 @@ open class UserAuthenticator { return canAuthenticate } + public func canAuthenticateViaBiometrics() -> Bool { + var error: NSError? + let canAuthenticate = LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + return canAuthenticate + } + open func authenticate(completion: ((AuthError?) -> Void)? = nil) { if state == .loggedIn { diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 487b6252a7..12831dd982 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -915,6 +915,7 @@ C12726F02A5FF89900215B02 /* EmailSignupPromptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12726EF2A5FF89900215B02 /* EmailSignupPromptViewModel.swift */; }; C12726F22A5FF8CB00215B02 /* EmailSignupPromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12726F12A5FF8CB00215B02 /* EmailSignupPromptViewController.swift */; }; C13B32D22A0E750700A59236 /* AutofillSettingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13B32D12A0E750700A59236 /* AutofillSettingStatus.swift */; }; + C13C076C2D00A6B7006386CF /* VaultCredentialManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13C076B2D00A6B7006386CF /* VaultCredentialManager.swift */; }; C13F3F682B7F88100083BE40 /* AuthConfirmationPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13F3F672B7F88100083BE40 /* AuthConfirmationPromptView.swift */; }; C13F3F6A2B7F883A0083BE40 /* AuthConfirmationPromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13F3F692B7F883A0083BE40 /* AuthConfirmationPromptViewController.swift */; }; C13F3F6C2B7F88470083BE40 /* AuthConfirmationPromptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13F3F6B2B7F88470083BE40 /* AuthConfirmationPromptViewModel.swift */; }; @@ -935,7 +936,6 @@ C1641EB12BC2F52B0012607A /* ImportPasswordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1641EB02BC2F52B0012607A /* ImportPasswordsView.swift */; }; C1641EB32BC2F53C0012607A /* ImportPasswordsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1641EB22BC2F53C0012607A /* ImportPasswordsViewModel.swift */; }; C174CE602BD6A6CE00AED2EA /* MockDDGSyncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C185ED652BD43A5500BAE9DC /* MockDDGSyncing.swift */; }; - C177D9F42CFDDFEB0039CBF7 /* AutofillPasswordFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C177D9F32CFDDFEB0039CBF7 /* AutofillPasswordFetcher.swift */; }; C177D9F62CFDDFEB0039CBF7 /* UIAlertControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C177D9F52CFDDFEB0039CBF7 /* UIAlertControllerExtension.swift */; }; C17B59592A03AAD30055F2D1 /* PasswordGenerationPromptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17B59562A03AAD30055F2D1 /* PasswordGenerationPromptViewModel.swift */; }; C17B595A2A03AAD30055F2D1 /* PasswordGenerationPromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17B59572A03AAD30055F2D1 /* PasswordGenerationPromptViewController.swift */; }; @@ -2793,6 +2793,7 @@ C12726EF2A5FF89900215B02 /* EmailSignupPromptViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignupPromptViewModel.swift; sourceTree = ""; }; C12726F12A5FF8CB00215B02 /* EmailSignupPromptViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignupPromptViewController.swift; sourceTree = ""; }; C13B32D12A0E750700A59236 /* AutofillSettingStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillSettingStatus.swift; sourceTree = ""; }; + C13C076B2D00A6B7006386CF /* VaultCredentialManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCredentialManager.swift; sourceTree = ""; }; C13F3F672B7F88100083BE40 /* AuthConfirmationPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthConfirmationPromptView.swift; sourceTree = ""; }; C13F3F692B7F883A0083BE40 /* AuthConfirmationPromptViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthConfirmationPromptViewController.swift; sourceTree = ""; }; C13F3F6B2B7F88470083BE40 /* AuthConfirmationPromptViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthConfirmationPromptViewModel.swift; sourceTree = ""; }; @@ -2811,7 +2812,6 @@ C1641EAE2BC2F5140012607A /* ImportPasswordsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportPasswordsViewController.swift; sourceTree = ""; }; C1641EB02BC2F52B0012607A /* ImportPasswordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportPasswordsView.swift; sourceTree = ""; }; C1641EB22BC2F53C0012607A /* ImportPasswordsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportPasswordsViewModel.swift; sourceTree = ""; }; - C177D9F32CFDDFEB0039CBF7 /* AutofillPasswordFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPasswordFetcher.swift; sourceTree = ""; }; C177D9F52CFDDFEB0039CBF7 /* UIAlertControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertControllerExtension.swift; sourceTree = ""; }; C17B59562A03AAD30055F2D1 /* PasswordGenerationPromptViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordGenerationPromptViewModel.swift; sourceTree = ""; }; C17B59572A03AAD30055F2D1 /* PasswordGenerationPromptViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordGenerationPromptViewController.swift; sourceTree = ""; }; @@ -5523,12 +5523,12 @@ C1CAAA6D2CF8BBBC00C37EE6 /* CredentialProvider */ = { isa = PBXGroup; children = ( - C1CAAAA42CFCBD6B00C37EE6 /* Shared */, - C1CAAA862CF9FFBE00C37EE6 /* CredentialProviderList */, C1EF5B252CC0457B002980E6 /* CredentialProviderViewController.swift */, C1CAAA682CF8B8C400C37EE6 /* CredentialProviderActivation */, + C1CAAA862CF9FFBE00C37EE6 /* CredentialProviderList */, C1CAAA6E2CF8BBC900C37EE6 /* Extensions */, C1CAAA742CF8BDC400C37EE6 /* Resources */, + C1CAAAA42CFCBD6B00C37EE6 /* Shared */, ); path = CredentialProvider; sourceTree = ""; @@ -5557,9 +5557,9 @@ C1CAAA862CF9FFBE00C37EE6 /* CredentialProviderList */ = { isa = PBXGroup; children = ( + C1CAAA912CFCAA0500C37EE6 /* CredentialProviderListItemTableViewCell.swift */, C1CAAA872CF9FFE100C37EE6 /* CredentialProviderListViewController.swift */, C1CAAA892CF9FFF300C37EE6 /* CredentialProviderListViewModel.swift */, - C1CAAA912CFCAA0500C37EE6 /* CredentialProviderListItemTableViewCell.swift */, C1CAAAA72CFCBE4800C37EE6 /* EmptySearchView.swift */, C1CAAAAB2CFCC91D00C37EE6 /* EmptyView.swift */, ); @@ -5589,7 +5589,7 @@ children = ( C1CAAAA52CFCBD7900C37EE6 /* LockScreenView.swift */, C1CAAAA92CFCC13E00C37EE6 /* SecureVaultReporter.swift */, - C177D9F32CFDDFEB0039CBF7 /* AutofillPasswordFetcher.swift */, + C13C076B2D00A6B7006386CF /* VaultCredentialManager.swift */, ); path = Shared; sourceTree = ""; @@ -8740,6 +8740,7 @@ C1CAAA712CF8BC0B00C37EE6 /* UIViewControllerExtension.swift in Sources */, C1CAAA6A2CF8BABF00C37EE6 /* CredentialProviderActivatedView.swift in Sources */, C1CAAA732CF8BD1C00C37EE6 /* CredentialProviderActivatedViewModel.swift in Sources */, + C13C076C2D00A6B7006386CF /* VaultCredentialManager.swift in Sources */, C1EF5B262CC0457B002980E6 /* CredentialProviderViewController.swift in Sources */, C1CAAA852CF8C9EA00C37EE6 /* UIResponderExtension.swift in Sources */, C1CAAA782CF8BDF200C37EE6 /* UserText.swift in Sources */, @@ -8748,7 +8749,6 @@ C1CAAA8A2CF9FFF300C37EE6 /* CredentialProviderListViewModel.swift in Sources */, C1CAAAA02CFCB7C200C37EE6 /* UImageExtension.swift in Sources */, C177D9F62CFDDFEB0039CBF7 /* UIAlertControllerExtension.swift in Sources */, - C177D9F42CFDDFEB0039CBF7 /* AutofillPasswordFetcher.swift in Sources */, C1CAAA882CF9FFE100C37EE6 /* CredentialProviderListViewController.swift in Sources */, C1CAAAAA2CFCC13E00C37EE6 /* SecureVaultReporter.swift in Sources */, C1CAAAA82CFCBE4800C37EE6 /* EmptySearchView.swift in Sources */, @@ -11774,7 +11774,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = revision; - revision = f99c7177385798611b62e8c98db92f3e390a1ce4; + revision = 4041588c3c0b9c6f519f4c4109806d6fcb7f77bd; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a44c5fefde..443e2b497b 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,7 +32,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "f99c7177385798611b62e8c98db92f3e390a1ce4" + "revision" : "4041588c3c0b9c6f519f4c4109806d6fcb7f77bd" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 0ec3c93bf1..eebeceda69 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -331,7 +331,8 @@ import os.log settingHandlers: [FavoritesDisplayModeSyncHandler()], favoritesDisplayModeStorage: FavoritesDisplayModeStorage(), syncErrorHandler: syncErrorHandler, - faviconStoring: Favicons.shared + faviconStoring: Favicons.shared, + tld: AppDependencyProvider.shared.storageCache.tld ) let syncService = DDGSync( diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 714ce3333f..3c267fda0d 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -88,6 +88,8 @@ final class AutofillLoginListViewModel: ObservableObject { return settings["monitorIntervalDays"] as? Int ?? 42 }() + private lazy var credentialIdentityStoreManager: AutofillCredentialIdentityStoreManaging = AutofillCredentialIdentityStoreManager(vault: secureVault, tld: tld) + private lazy var syncPromoManager: SyncPromoManaging = SyncPromoManager(syncService: syncService) private lazy var autofillSurveyManager: AutofillSurveyManaging = AutofillSurveyManager() @@ -380,7 +382,13 @@ final class AutofillLoginListViewModel: ObservableObject { } do { - return try secureVault.accounts() + let accounts = try secureVault.accounts() + + Task { + await credentialIdentityStoreManager.replaceCredentialStore(with: accounts) + } + + return accounts } catch { Logger.autofill.error("Failed to fetch accounts \(error.localizedDescription, privacy: .public)") return [] diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index bc248cd104..7e442f233f 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -274,7 +274,16 @@ class TabViewController: UIViewController { manager.delegate = self return manager }() - + + private lazy var credentialIdentityStoreManager: AutofillCredentialIdentityStoreManager? = { + guard let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) else { + return nil + } + + return AutofillCredentialIdentityStoreManager(vault: vault, + tld: AppDependencyProvider.shared.storageCache.tld) + }() + private static let debugEvents = EventMapping { event, _, _, onComplete in let domainEvent: Pixel.Event switch event { @@ -2833,8 +2842,13 @@ extension TabViewController: SecureVaultManagerDelegate { if accounts.count > 0 { let accountMatches = autofillWebsiteAccountMatcher.findDeduplicatedSortedMatches(accounts: accounts, for: domain) - presentAutofillPromptViewController(accountMatches: accountMatches, domain: domain, trigger: trigger, useLargeDetent: false) { account in + presentAutofillPromptViewController(accountMatches: accountMatches, domain: domain, trigger: trigger, useLargeDetent: false) { [weak self] account in onAccountSelected(account) + + guard let domain = account?.domain else { return } + Task { + await self?.credentialIdentityStoreManager?.updateCredentialStore(for: domain) + } } completionHandler: { account in if account != nil { NotificationCenter.default.post(name: .autofillFillEvent, object: nil) @@ -3028,6 +3042,11 @@ extension TabViewController: SaveLoginViewControllerDelegate { }) Favicons.shared.loadFavicon(forDomain: newCredential.account.domain, intoCache: .fireproof, fromCache: .tabs) } + + guard let domain = newCredential.account.domain else { return } + Task { + await credentialIdentityStoreManager?.updateCredentialStore(for: domain) + } } } catch { Logger.general.error("failed to fetch credentials: \(error.localizedDescription, privacy: .public)") diff --git a/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift b/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift index f7f47e4602..06e0523e4b 100644 --- a/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift +++ b/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift @@ -27,6 +27,7 @@ import RemoteMessaging import Configuration import Core import SubscriptionTestingUtilities +import Common @testable import DuckDuckGo final class OnboardingDaxFavouritesTests: XCTestCase { @@ -47,7 +48,8 @@ final class OnboardingDaxFavouritesTests: XCTestCase { settingHandlers: [], favoritesDisplayModeStorage: MockFavoritesDisplayModeStoring(), syncErrorHandler: SyncErrorHandler(), - faviconStoring: MockFaviconStore() + faviconStoring: MockFaviconStore(), + tld: TLD() ) let remoteMessagingClient = RemoteMessagingClient( diff --git a/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift b/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift index 8914c8d638..63303472e0 100644 --- a/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift +++ b/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift @@ -27,6 +27,7 @@ import RemoteMessaging import Configuration import Combine import SubscriptionTestingUtilities +import Common @testable import DuckDuckGo @testable import Core @@ -47,7 +48,8 @@ final class OnboardingNavigationDelegateTests: XCTestCase { settingHandlers: [], favoritesDisplayModeStorage: MockFavoritesDisplayModeStoring(), syncErrorHandler: SyncErrorHandler(), - faviconStoring: MockFaviconStore() + faviconStoring: MockFaviconStore(), + tld: TLD() ) let remoteMessagingClient = RemoteMessagingClient( diff --git a/DuckDuckGoTests/SyncCredentialsAdapterTests.swift b/DuckDuckGoTests/SyncCredentialsAdapterTests.swift index 3f6f7635ac..78b12db40c 100644 --- a/DuckDuckGoTests/SyncCredentialsAdapterTests.swift +++ b/DuckDuckGoTests/SyncCredentialsAdapterTests.swift @@ -23,6 +23,7 @@ import Combine import DDGSync import SecureStorage import Core +import Common @testable import DuckDuckGo final class SyncCredentialsAdapterTests: XCTestCase { @@ -34,7 +35,7 @@ final class SyncCredentialsAdapterTests: XCTestCase { override func setUpWithError() throws { errorHandler = CapturingAdapterErrorHandler() - adapter = SyncCredentialsAdapter(secureVaultErrorReporter: MockSecureVaultReporting(), syncErrorHandler: errorHandler) + adapter = SyncCredentialsAdapter(secureVaultErrorReporter: MockSecureVaultReporting(), syncErrorHandler: errorHandler, tld: TLD()) cancellables = [] } diff --git a/DuckDuckGoTests/SyncSettingsViewControllerErrorTests.swift b/DuckDuckGoTests/SyncSettingsViewControllerErrorTests.swift index d929969a45..0233f92598 100644 --- a/DuckDuckGoTests/SyncSettingsViewControllerErrorTests.swift +++ b/DuckDuckGoTests/SyncSettingsViewControllerErrorTests.swift @@ -23,6 +23,7 @@ import Core import Combine import DDGSync import Persistence +import Common final class SyncSettingsViewControllerErrorTests: XCTestCase { @@ -53,7 +54,8 @@ final class SyncSettingsViewControllerErrorTests: XCTestCase { faviconStoring: MockFaviconStore()) let credentialsAdapter = SyncCredentialsAdapter( secureVaultErrorReporter: MockSecureVaultReporting(), - syncErrorHandler: CapturingAdapterErrorHandler()) + syncErrorHandler: CapturingAdapterErrorHandler(), + tld: TLD()) vc = SyncSettingsViewController( syncService: ddgSyncing, syncBookmarksAdapter: bookmarksAdapter, diff --git a/alphaAdhocExportOptions.plist b/alphaAdhocExportOptions.plist index faed54b818..d733a242c0 100644 --- a/alphaAdhocExportOptions.plist +++ b/alphaAdhocExportOptions.plist @@ -20,6 +20,8 @@ match AdHoc com.duckduckgo.mobile.ios.alpha.Widgets com.duckduckgo.mobile.ios.alpha.NetworkExtension match AdHoc com.duckduckgo.mobile.ios.alpha.NetworkExtension + com.duckduckgo.mobile.ios.alpha.CredentialExtension + match AdHoc com.duckduckgo.mobile.ios.alpha.CredentialExtension diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1d0d12d932..c185c7bfed 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -58,6 +58,10 @@ lane :release_adhoc do |options| { targets: ["PacketTunnelProvider"], profile_name: "match AdHoc com.duckduckgo.mobile.ios.NetworkExtension" + }, + { + targets: ["AutofillCredentialProvider"], + profile_name: "match AdHoc com.duckduckgo.mobile.ios.CredentialExtension" } ] @@ -125,6 +129,10 @@ lane :alpha_adhoc do |options| { targets: ["PacketTunnelProvider"], profile_name: "match AdHoc com.duckduckgo.mobile.ios.alpha.NetworkExtension" + }, + { + targets: ["AutofillCredentialProvider"], + profile_name: "match AdHoc com.duckduckgo.mobile.ios.alpha.CredentialExtension" } ] diff --git a/fastlane/Matchfile b/fastlane/Matchfile index 022ff22969..4212b95ef5 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -14,7 +14,7 @@ end for_lane :sync_signing_alpha_adhoc do type "adhoc" - app_identifier ["com.duckduckgo.mobile.ios.alpha", "com.duckduckgo.mobile.ios.alpha.ShareExtension", "com.duckduckgo.mobile.ios.alpha.OpenAction2", "com.duckduckgo.mobile.ios.alpha.Widgets", "com.duckduckgo.mobile.ios.alpha.NetworkExtension"] + app_identifier ["com.duckduckgo.mobile.ios.alpha", "com.duckduckgo.mobile.ios.alpha.ShareExtension", "com.duckduckgo.mobile.ios.alpha.OpenAction2", "com.duckduckgo.mobile.ios.alpha.Widgets", "com.duckduckgo.mobile.ios.alpha.NetworkExtension", "com.duckduckgo.mobile.ios.alpha.CredentialExtension"] force_for_new_devices true template_name "Default Web Browser iOS (Dist)" end @@ -34,7 +34,7 @@ end for_lane :alpha_adhoc do type "adhoc" - app_identifier ["com.duckduckgo.mobile.ios.alpha", "com.duckduckgo.mobile.ios.alpha.ShareExtension", "com.duckduckgo.mobile.ios.alpha.OpenAction2", "com.duckduckgo.mobile.ios.alpha.Widgets", "com.duckduckgo.mobile.ios.alpha.NetworkExtension"] + app_identifier ["com.duckduckgo.mobile.ios.alpha", "com.duckduckgo.mobile.ios.alpha.ShareExtension", "com.duckduckgo.mobile.ios.alpha.OpenAction2", "com.duckduckgo.mobile.ios.alpha.Widgets", "com.duckduckgo.mobile.ios.alpha.NetworkExtension", "com.duckduckgo.mobile.ios.alpha.CredentialExtension"] force_for_new_devices true template_name "Default Web Browser iOS (Dist)" end