Skip to content

Commit

Permalink
Autofill never save for site (#1991)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1201645688934642/1205190244970075/f
Tech Design URL:
CC:

Description:
Adds the functionality to allow users to choose to "Never Save for this Site" from the autofill save password prompt, with the option to reset this list from the Settings screen.
  • Loading branch information
amddg44 authored Dec 28, 2023
1 parent 3e020ba commit 129d140
Show file tree
Hide file tree
Showing 20 changed files with 274 additions and 37 deletions.
10 changes: 9 additions & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2969,6 +2969,9 @@
BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; };
BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; };
BBDFDC5D2B2B8E2100F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; };
C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */; };
C168B9AD2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */; };
C168B9AE2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */; };
CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */; };
CB24F70D29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */; };
CB6BCDF927C6BEFF00CC76DC /* PrivacyFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */; };
Expand Down Expand Up @@ -4269,6 +4272,7 @@
B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = "<group>"; };
BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = "<group>"; };
BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionExternalWaitlistPixels.swift; sourceTree = "<group>"; };
C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillNeverPromptWebsitesManager.swift; sourceTree = "<group>"; };
CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationURLProvider.swift; sourceTree = "<group>"; };
CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyFeatures.swift; sourceTree = "<group>"; };
CBDD5DE229A67F2700832877 /* MockConfigurationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockConfigurationStore.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6220,6 +6224,7 @@
4BE6547A271FCD4D008D1D63 /* PasswordManagementIdentityModel.swift */,
4BE6547C271FCD4D008D1D63 /* PasswordManagementLoginModel.swift */,
4BE6547D271FCD4D008D1D63 /* PasswordManagementNoteModel.swift */,
C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */,
);
path = Model;
sourceTree = "<group>";
Expand Down Expand Up @@ -9529,6 +9534,7 @@
3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */,
3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */,
4B67854B2AA8DE76008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */,
C168B9AD2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */,
37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */,
3706FB75293F65D500E42796 /* WebsiteBreakageSender.swift in Sources */,
3707C72A294B5D2900682A9F /* URLExtension.swift in Sources */,
Expand Down Expand Up @@ -10840,6 +10846,7 @@
4B957B0A2AC7AE700062CA31 /* Pixel.swift in Sources */,
4B957B0B2AC7AE700062CA31 /* PixelEvent.swift in Sources */,
4B957B0C2AC7AE700062CA31 /* TabBarFooter.swift in Sources */,
C168B9AE2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */,
4B957B0D2AC7AE700062CA31 /* JSAlertViewModel.swift in Sources */,
4B957B0E2AC7AE700062CA31 /* BookmarksBarCollectionViewItem.swift in Sources */,
4B957B0F2AC7AE700062CA31 /* FileDownloadError.swift in Sources */,
Expand Down Expand Up @@ -11616,6 +11623,7 @@
85AC3B0525D6B1D800C7D2AA /* ScriptSourceProviding.swift in Sources */,
4BB99D0026FE191E001E4761 /* CoreDataBookmarkImporter.swift in Sources */,
4BCF15D72ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */,
C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */,
D64A5FF82AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */,
37A6A8F62AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */,
AA3F895324C18AD500628DDE /* SuggestionViewModel.swift in Sources */,
Expand Down Expand Up @@ -13050,7 +13058,7 @@
repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit";
requirement = {
kind = exactVersion;
version = 100.0.0;
version = 100.0.1;
};
};
AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/BrowserServicesKit",
"state" : {
"revision" : "c3a482a4ca22d706207d08a68db8f23f0c262040",
"version" : "100.0.0"
"revision" : "e9c344c15d550112d02853c07efeaa154e911e2b",
"version" : "100.0.1"
}
},
{
Expand Down
6 changes: 6 additions & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct UserText {
static let ok = NSLocalizedString("ok", value: "OK", comment: "OK button")
static let cancel = NSLocalizedString("cancel", value: "Cancel", comment: "Cancel button")
static let notNow = NSLocalizedString("notnow", value: "Not Now", comment: "Not Now button")
static let neverForThisSite = NSLocalizedString("never.for.this.site", value: "Never Ask for This Site", comment: "Never ask to save login credentials for this site button")
static let open = NSLocalizedString("open", value: "Open", comment: "Open button")
static let save = NSLocalizedString("save", value: "Save", comment: "Save button")
static let copy = NSLocalizedString("copy", value: "Copy", comment: "Copy button")
Expand Down Expand Up @@ -396,6 +397,11 @@ struct UserText {
static let autofillUsernamesAndPasswords = NSLocalizedString("autofill.usernames-and-passwords", value: "Usernames and passwords", comment: "Autofill autosaved data type")
static let autofillAddresses = NSLocalizedString("autofill.addresses", value: "Addresses", comment: "Autofill autosaved data type")
static let autofillPaymentMethods = NSLocalizedString("autofill.payment-methods", value: "Payment methods", comment: "Autofill autosaved data type")
static let autofillExcludedSites = NSLocalizedString("autofill.excluded-sites", value: "Excluded Sites", comment: "Autofill settings section title")
static let autofillExcludedSitesExplanation = NSLocalizedString("autofill.excluded-sites.explanation", value: "Websites you selected to never ask to save your password.", comment: "Subtitle providing additional information about the excluded sites section")
static let autofillExcludedSitesReset = NSLocalizedString("autofill.excluded-sites.reset", value: "Reset", comment: "Button title allowing users to reset their list of excluded sites")
static let autofillExcludedSitesResetActionTitle = NSLocalizedString("autofill.excluded-sites.reset.action.title", value:"Reset Excluded Sites?", comment: "Alert title")
static let autofillExcludedSitesResetActionMessage = NSLocalizedString("autofill.excluded-sites.reset.action.message", value:"If you reset excluded sites, you will be prompted to save your password next time you sign in to any of these sites.", comment: "Alert title")
static let autofillAutoLock = NSLocalizedString("autofill.auto-lock", value: "Auto-lock", comment: "Autofill settings section title")
static let autofillLockWhenIdle = NSLocalizedString("autofill.lock-when-idle", value: "Lock autofill after computer is idle for", comment: "Autofill auto-lock setting")
static let autofillNeverLock = NSLocalizedString("autofill.never-lock", value: "Never lock autofill", comment: "Autofill auto-lock setting")
Expand Down
13 changes: 12 additions & 1 deletion DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ final class AutofillPreferencesModel: ObservableObject {

@Published private(set) var isBitwardenSetupFlowPresented = false

@Published private(set) var hasNeverPromptWebsites: Bool = false

func authorizeAutoLockSettingsChange(
isEnabled isAutoLockEnabledNewValue: Bool? = nil,
threshold autoLockThresholdNewValue: AutofillAutoLockThreshold? = nil
Expand Down Expand Up @@ -125,15 +127,22 @@ final class AutofillPreferencesModel: ObservableObject {
navigationViewController.showPasswordManagerPopover(selectedCategory: selectedCategory)
}

func resetNeverPromptWebsites() {
_ = neverPromptWebsitesManager.deleteAllNeverPromptWebsites()
hasNeverPromptWebsites = !neverPromptWebsitesManager.neverPromptWebsites.isEmpty
}

@MainActor
init(
persistor: AutofillPreferencesPersistor = AutofillPreferences(),
userAuthenticator: UserAuthenticating = DeviceAuthenticator.shared,
bitwardenInstallationService: BWInstallationService = LocalBitwardenInstallationService()
bitwardenInstallationService: BWInstallationService = LocalBitwardenInstallationService(),
neverPromptWebsitesManager: AutofillNeverPromptWebsitesManager = AutofillNeverPromptWebsitesManager.shared
) {
self.persistor = persistor
self.userAuthenticator = userAuthenticator
self.bitwardenInstallationService = bitwardenInstallationService
self.neverPromptWebsitesManager = neverPromptWebsitesManager

isAutoLockEnabled = persistor.isAutoLockEnabled
autoLockThreshold = persistor.autoLockThreshold
Expand All @@ -143,11 +152,13 @@ final class AutofillPreferencesModel: ObservableObject {
askToSavePaymentMethods = persistor.askToSavePaymentMethods
autolockLocksFormFilling = persistor.autolockLocksFormFilling
passwordManager = persistor.passwordManager
hasNeverPromptWebsites = !neverPromptWebsitesManager.neverPromptWebsites.isEmpty
}

private var persistor: AutofillPreferencesPersistor
private var userAuthenticator: UserAuthenticating
private let bitwardenInstallationService: BWInstallationService
private let neverPromptWebsitesManager: AutofillNeverPromptWebsitesManager

// MARK: - Password Manager

Expand Down
67 changes: 66 additions & 1 deletion DuckDuckGo/Preferences/View/PreferencesAutofillView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension Preferences {
struct AutofillView: View {
@ObservedObject var model: AutofillPreferencesModel
@ObservedObject var bitwardenManager = BWManager.shared
@State private var showingResetNeverPromptSitesSheet = false

var passwordManagerBinding: Binding<PasswordManager> {
.init {
Expand Down Expand Up @@ -117,7 +118,25 @@ extension Preferences {
TextMenuItemCaption(text: UserText.autofillAskToSaveExplanation)
}

// SECTION 3: Auto-Lock:
// SECTION 3: Reset excluded (aka never prompt to save) sites:
// This is only displayed if the user has never prompt sites saved & not using Bitwarden
if model.hasNeverPromptWebsites && model.passwordManager == .duckduckgo {
PreferencePaneSection {
TextMenuItemHeader(text: UserText.autofillExcludedSites)
TextMenuItemCaption(text: UserText.autofillExcludedSitesExplanation)
.padding(.top, -8)
Button(UserText.autofillExcludedSitesReset) {
showingResetNeverPromptSitesSheet.toggle()
if showingResetNeverPromptSitesSheet {
Pixel.fire(.autofillLoginsSettingsResetExcludedDisplayed)
}
}
}.sheet(isPresented: $showingResetNeverPromptSitesSheet) {
ResetNeverPromptSitesSheet(autofillPreferencesModel: model, isSheetPresented: $showingResetNeverPromptSitesSheet)
}
}

// SECTION 4: Auto-Lock:

PreferencePaneSection {
TextMenuItemHeader(text: UserText.autofillAutoLock)
Expand Down Expand Up @@ -289,3 +308,49 @@ private struct BitwardenStatusView: View {
}

}

struct ResetNeverPromptSitesSheet: View {

@ObservedObject var autofillPreferencesModel: AutofillPreferencesModel
@Binding var isSheetPresented: Bool

var body: some View {
VStack(alignment: .center) {
Text(UserText.autofillExcludedSitesResetActionTitle)
.font(Preferences.Const.Fonts.preferencePaneTitle)
.padding(.top, 10)

Text(UserText.autofillExcludedSitesResetActionMessage)
.padding(.vertical, 10)
.padding(.horizontal, 20)
.frame(width: 300)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)

Divider()

HStack(alignment: .center) {
Spacer()
Button(UserText.cancel) {
isSheetPresented.toggle()
Pixel.fire(.autofillLoginsSettingsResetExcludedDismissed)
}
Button(action: {
saveChanges()
}, label: {
Text(UserText.autofillExcludedSitesReset)
.foregroundColor(.red)
})
}.padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 15))

}
.padding(.vertical, 10)
}

private func saveChanges() {
autofillPreferencesModel.resetNeverPromptWebsites()
isSheetPresented.toggle()
Pixel.fire(.autofillLoginsSettingsResetExcludedConfirmed)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// AutofillNeverPromptWebsitesManager.swift
//
// Copyright © 2023 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 BrowserServicesKit

final class AutofillNeverPromptWebsitesManager {

static let shared = AutofillNeverPromptWebsitesManager()

public private(set) var neverPromptWebsites: [SecureVaultModels.NeverPromptWebsites] = []

private let secureVault: (any AutofillSecureVault)?

public init(secureVault: (any AutofillSecureVault)? = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared)) {
self.secureVault = secureVault

fetchNeverPromptWebsites()
}

public func hasNeverPromptWebsitesFor(domain: String) -> Bool {
return neverPromptWebsites.contains { $0.domain == domain }
}

public func saveNeverPromptWebsite(_ domain: String) throws -> Int64? {
guard let secureVault = secureVault else {
return nil
}

do {
let id = try secureVault.storeNeverPromptWebsites(SecureVaultModels.NeverPromptWebsites(domain: domain))

fetchNeverPromptWebsites()
return id
} catch {
Pixel.fire(.debug(event: .secureVaultError, error: error))
throw error
}
}

public func deleteAllNeverPromptWebsites() -> Bool {
guard let secureVault = secureVault else {
return false
}

do {
try secureVault.deleteAllNeverPromptWebsites()

fetchNeverPromptWebsites()
return true
} catch {
Pixel.fire(.debug(event: .secureVaultError, error: error))
return false
}
}

private func fetchNeverPromptWebsites() {
guard let secureVault = secureVault else {
return
}

do {
neverPromptWebsites = try secureVault.neverPromptWebsites()
} catch {
Pixel.fire(.debug(event: .secureVaultError, error: error))
neverPromptWebsites = []
}
}
}
Loading

0 comments on commit 129d140

Please sign in to comment.