diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index be147b4f12..ab5be088cd 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -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 */; }; @@ -4269,6 +4272,7 @@ B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = ""; }; BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionExternalWaitlistPixels.swift; sourceTree = ""; }; + C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillNeverPromptWebsitesManager.swift; sourceTree = ""; }; CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationURLProvider.swift; sourceTree = ""; }; CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyFeatures.swift; sourceTree = ""; }; CBDD5DE229A67F2700832877 /* MockConfigurationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockConfigurationStore.swift; sourceTree = ""; }; @@ -6220,6 +6224,7 @@ 4BE6547A271FCD4D008D1D63 /* PasswordManagementIdentityModel.swift */, 4BE6547C271FCD4D008D1D63 /* PasswordManagementLoginModel.swift */, 4BE6547D271FCD4D008D1D63 /* PasswordManagementNoteModel.swift */, + C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */, ); path = Model; sourceTree = ""; @@ -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 */, @@ -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 */, @@ -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 */, @@ -13050,7 +13058,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 100.0.0; + version = 100.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 96ffc4f465..969ad6baa7 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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" } }, { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 91926817f7..5c05a69d7e 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -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") @@ -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") diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift index c7f246c0ee..a78072a562 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift @@ -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 @@ -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 @@ -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 diff --git a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift index 5421aa0ff9..8e349919b9 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift @@ -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 { .init { @@ -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) @@ -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) + } + +} diff --git a/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift b/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift new file mode 100644 index 0000000000..60d8eb0309 --- /dev/null +++ b/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift @@ -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 = [] + } + } +} diff --git a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard index 0f53f9b381..b1e5940b87 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard +++ b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard @@ -617,19 +617,22 @@ DQ - +