diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 8bf183dafd..7ec10ba680 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -32,6 +32,7 @@ public enum FeatureFlag: String { case autofillFailureReporting case autofillOnForExistingUsers case autofillUnknownUsernameCategorization + case autofillPartialFormSaves case incontextSignup case autoconsentOnByDefault case history @@ -101,6 +102,8 @@ extension FeatureFlag: FeatureFlagDescribing { return .remoteReleasable(.subfeature(AutofillSubfeature.onForExistingUsers)) case .autofillUnknownUsernameCategorization: return .remoteReleasable(.subfeature(AutofillSubfeature.unknownUsernameCategorization)) + case .autofillPartialFormSaves: + return .remoteReleasable(.subfeature(AutofillSubfeature.partialFormSaves)) case .incontextSignup: return .remoteReleasable(.feature(.incontextSignup)) case .autoconsentOnByDefault: diff --git a/Core/Pixel.swift b/Core/Pixel.swift index 04f48ee03f..ded3c22a2c 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -147,6 +147,7 @@ public struct PixelParameters { // Autofill public static let countBucket = "count_bucket" + public static let backfilled = "backfilled" // Privacy Dashboard public static let daysSinceInstall = "daysSinceInstall" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 65b869efe7..b7569472d1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11168,7 +11168,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 218.1.0; + version = 219.0.0; }; }; 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 3c92013c76..f2ec97c5fa 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "a82c14b991f8cbef46d4b7d8e613762f1592fcd2", - "version" : "218.1.0" + "revision" : "20df9e22d5a69acfc25204fb0228a9d6f79b721f", + "version" : "219.0.0" } }, { diff --git a/DuckDuckGo/EmailSignupViewController.swift b/DuckDuckGo/EmailSignupViewController.swift index 056f861310..19b89ceb11 100644 --- a/DuckDuckGo/EmailSignupViewController.swift +++ b/DuckDuckGo/EmailSignupViewController.swift @@ -64,6 +64,7 @@ class EmailSignupViewController: UIViewController { lazy private var vaultManager: SecureVaultManager = { let manager = SecureVaultManager(includePartialAccountMatches: true, + shouldAllowPartialFormSaves: featureFlagger.isFeatureOn(.autofillPartialFormSaves), tld: AppDependencyProvider.shared.storageCache.tld) manager.delegate = self return manager diff --git a/DuckDuckGo/SaveAutofillLoginManager.swift b/DuckDuckGo/SaveAutofillLoginManager.swift index ea932d0ce0..4749b6dfd4 100644 --- a/DuckDuckGo/SaveAutofillLoginManager.swift +++ b/DuckDuckGo/SaveAutofillLoginManager.swift @@ -29,6 +29,7 @@ protocol SaveAutofillLoginManagerProtocol { var isPasswordOnlyAccount: Bool { get } var hasOtherCredentialsOnSameDomain: Bool { get } var hasSavedMatchingPasswordWithoutUsername: Bool { get } + var hasSavedMatchingUsernameWithoutPassword: Bool { get } var hasSavedMatchingUsername: Bool { get } static func saveCredentials(_ credentials: SecureVaultModels.WebsiteCredentials, with factory: AutofillVaultFactory) throws -> Int64 @@ -85,7 +86,15 @@ final class SaveAutofillLoginManager: SaveAutofillLoginManagerProtocol { var hasSavedMatchingUsername: Bool { savedMatchingUsernameCredential != nil } - + + var hasSavedMatchingUsernameWithoutPassword: Bool { + if let savedMatchingUsernameCredential { + return savedMatchingUsernameCredential.password.flatMap { String(data: $0, encoding: .utf8) }.isNilOrEmpty + } else { + return false + } + } + func prepareData(completion: @escaping () -> Void) { fetchDomainStoredCredentials { [weak self] credentials in self?.domainStoredCredentials = credentials @@ -112,7 +121,7 @@ final class SaveAutofillLoginManager: SaveAutofillLoginManagerProtocol { let credentialsWithSameUsername = domainStoredCredentials.filter { $0.account.username == credentials.account.username } return credentialsWithSameUsername.first } - + private func fetchDomainStoredCredentials(completion: @escaping ([SecureVaultModels.WebsiteCredentials]) -> Void) { DispatchQueue.global(qos: .userInteractive).async { var result = [SecureVaultModels.WebsiteCredentials]() diff --git a/DuckDuckGo/SaveLoginView.swift b/DuckDuckGo/SaveLoginView.swift index 2a738213d0..1b624ca3c4 100644 --- a/DuckDuckGo/SaveLoginView.swift +++ b/DuckDuckGo/SaveLoginView.swift @@ -264,6 +264,7 @@ private enum Const { struct SaveLoginView_Previews: PreviewProvider { private struct MockManager: SaveAutofillLoginManagerProtocol { + var hasSavedMatchingUsernameWithoutPassword: Bool { false } var username: String { "dax@duck.com" } var visiblePassword: String { "supersecurepasswordquack" } diff --git a/DuckDuckGo/SaveLoginViewController.swift b/DuckDuckGo/SaveLoginViewController.swift index 610e8bb0fb..31dad0e309 100644 --- a/DuckDuckGo/SaveLoginViewController.swift +++ b/DuckDuckGo/SaveLoginViewController.swift @@ -77,9 +77,11 @@ class SaveLoginViewController: UIViewController { case .savePassword: Pixel.fire(pixel: .autofillLoginsSavePasswordModalDismissed) case .updateUsername: - Pixel.fire(pixel: .autofillLoginsUpdateUsernameModalDismissed) + let isBackfilled = viewModel.isUpdatingEmptyUsername + Pixel.fire(pixel: .autofillLoginsUpdateUsernameModalDismissed, withAdditionalParameters: [PixelParameters.backfilled: String(describing: isBackfilled)]) case .updatePassword: - Pixel.fire(pixel: .autofillLoginsUpdatePasswordModalDismissed) + let isBackfilled = viewModel.isUpdatingEmptyPassword + Pixel.fire(pixel: .autofillLoginsUpdatePasswordModalDismissed, withAdditionalParameters: [PixelParameters.backfilled: String(describing: isBackfilled)]) } viewModel.viewControllerDidDisappear() @@ -103,9 +105,11 @@ class SaveLoginViewController: UIViewController { case .savePassword: Pixel.fire(pixel: .autofillLoginsSavePasswordModalDisplayed) case .updateUsername: - Pixel.fire(pixel: .autofillLoginsUpdateUsernameModalDisplayed) + let isBackfilled = saveViewModel.isUpdatingEmptyUsername + Pixel.fire(pixel: .autofillLoginsUpdateUsernameModalDisplayed, withAdditionalParameters: [PixelParameters.backfilled: String(describing: isBackfilled)]) case .updatePassword: - Pixel.fire(pixel: .autofillLoginsUpdatePasswordModalDisplayed) + let isBackfilled = saveViewModel.isUpdatingEmptyPassword + Pixel.fire(pixel: .autofillLoginsUpdatePasswordModalDisplayed, withAdditionalParameters: [PixelParameters.backfilled: String(describing: isBackfilled)]) } } } @@ -124,9 +128,11 @@ extension SaveLoginViewController: SaveLoginViewModelDelegate { delegate?.saveLoginViewController(self, didSaveCredentials: credentialManager.credentials) case .updatePassword, .updateUsername: if viewModel.layoutType == .updatePassword { - Pixel.fire(pixel: .autofillLoginsUpdatePasswordModalConfirmed) + let isBackfilled = viewModel.isUpdatingEmptyPassword + Pixel.fire(pixel: .autofillLoginsUpdatePasswordModalConfirmed, withAdditionalParameters: [PixelParameters.backfilled: String(describing: isBackfilled)]) } else { - Pixel.fire(pixel: .autofillLoginsUpdateUsernameModalConfirmed) + let isBackfilled = viewModel.isUpdatingEmptyUsername + Pixel.fire(pixel: .autofillLoginsUpdateUsernameModalConfirmed, withAdditionalParameters: [PixelParameters.backfilled: String(describing: isBackfilled)]) } delegate?.saveLoginViewController(self, didUpdateCredentials: credentialManager.credentials) } diff --git a/DuckDuckGo/SaveLoginViewModel.swift b/DuckDuckGo/SaveLoginViewModel.swift index d8adba63dc..b1e57ef935 100644 --- a/DuckDuckGo/SaveLoginViewModel.swift +++ b/DuckDuckGo/SaveLoginViewModel.swift @@ -88,10 +88,14 @@ final class SaveLoginViewModel: ObservableObject { credentialManager.hasSavedMatchingUsername } - var isUpdatingUsername: Bool { + var isUpdatingEmptyUsername: Bool { credentialManager.hasSavedMatchingPasswordWithoutUsername } + var isUpdatingEmptyPassword: Bool { + credentialManager.hasSavedMatchingUsernameWithoutPassword + } + var hiddenPassword: String { PasswordHider(password: credentialManager.visiblePassword).hiddenPassword } @@ -132,7 +136,7 @@ final class SaveLoginViewModel: ObservableObject { return .savePassword } - if isUpdatingUsername { + if isUpdatingEmptyUsername { return .updateUsername } diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 5263815bf0..27f924cc7f 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -270,6 +270,7 @@ class TabViewController: UIViewController { lazy var vaultManager: SecureVaultManager = { let manager = SecureVaultManager(includePartialAccountMatches: true, + shouldAllowPartialFormSaves: featureFlagger.isFeatureOn(.autofillPartialFormSaves), tld: AppDependencyProvider.shared.storageCache.tld) manager.delegate = self return manager