Skip to content

Commit

Permalink
Increase ratio of complete form saves (#3698)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1206048666874235/f

**Description**:

Increase ratio of complete credentials saves (i.e. that include the
username) by checking for recently filled emails/usernames when a form
save event is incomplete.

Approach is to have a new form save events for username/email-only forms
that stays in memory for 3 minutes. If a password-only form save is
detected for the same domain, we can complete the form with the previous
partial data. This would also cover mobile where we don't have
identities autofill – and in general in all cases where the user inputs
data manually.

**Steps to test this PR**:
https://app.asana.com/0/1202926619870900/1208866651723703/f

**Definition of Done (Internal Only)**:

* [ ] Does this PR satisfy our [Definition of
Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)?

**Copy Testing**:

* [ ] Use of correct apostrophes in new copy, ie `’` rather than `'`

**Orientation Testing**:

* [ ] Portrait
* [ ] Landscape

**Device Testing**:

* [ ] iPhone SE (1st Gen)
* [ ] iPhone 8
* [ ] iPhone X
* [ ] iPhone 14 Pro
* [ ] iPad

**OS Testing**:

* [ ] iOS 15
* [ ] iOS 16
* [ ] iOS 17

**Theme Testing**:

* [ ] Light theme
* [ ] Dark theme

---
###### Internal references:
[Software Engineering
Expectations](https://app.asana.com/0/59792373528535/199064865822552)
[Technical Design
Template](https://app.asana.com/0/59792373528535/184709971311943)
  • Loading branch information
graeme authored Dec 9, 2024
1 parent 6b18d19 commit d791a00
Show file tree
Hide file tree
Showing 10 changed files with 39 additions and 13 deletions.
3 changes: 3 additions & 0 deletions Core/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public enum FeatureFlag: String {
case autofillFailureReporting
case autofillOnForExistingUsers
case autofillUnknownUsernameCategorization
case autofillPartialFormSaves
case incontextSignup
case autoconsentOnByDefault
case history
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions Core/Pixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
{
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/EmailSignupViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions DuckDuckGo/SaveAutofillLoginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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]()
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/SaveLoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ private enum Const {

struct SaveLoginView_Previews: PreviewProvider {
private struct MockManager: SaveAutofillLoginManagerProtocol {
var hasSavedMatchingUsernameWithoutPassword: Bool { false }

var username: String { "[email protected]" }
var visiblePassword: String { "supersecurepasswordquack" }
Expand Down
18 changes: 12 additions & 6 deletions DuckDuckGo/SaveLoginViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)])
}
}
}
Expand All @@ -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)
}
Expand Down
8 changes: 6 additions & 2 deletions DuckDuckGo/SaveLoginViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -132,7 +136,7 @@ final class SaveLoginViewModel: ObservableObject {
return .savePassword
}

if isUpdatingUsername {
if isUpdatingEmptyUsername {
return .updateUsername
}

Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/TabViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d791a00

Please sign in to comment.