From 0d414c6ac329e2cc7b6cd6167cff4f0c2a0f79cf Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 22 Nov 2024 14:01:53 +0100 Subject: [PATCH 1/6] wip --- Core/UserDefaultsPropertyWrapper.swift | 3 + DuckDuckGo.xcodeproj/project.pbxproj | 4 + DuckDuckGo/SettingsGeneralView.swift | 13 +++ .../SettingsViewModel+ThreatDetection.swift | 80 +++++++++++++++++++ DuckDuckGo/SettingsViewModel.swift | 1 + 5 files changed, 101 insertions(+) create mode 100644 DuckDuckGo/SettingsViewModel+ThreatDetection.swift diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 4b0b9682ab..d78d63aef0 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -187,6 +187,9 @@ public struct UserDefaultsWrapper { // TipKit case resetTipKitOnNextLaunch = "com.duckduckgo.ios.tipKit.resetOnNextLaunch" + + // Malicious Site Protection + case maliciousSiteProtectionEnabled = "com.duckduckgo.ios.maliciousSiteProtection.enabled" } private let key: Key diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f72d9ddf12..aac8eeee32 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -820,6 +820,7 @@ 9F9A92342C86B42B001D036D /* AppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A92332C86B42B001D036D /* AppIconPicker.swift */; }; 9F9EE4CE2C377D4900D4118E /* OnboardingFirePixelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */; }; 9F9EE4D42C37BB1300D4118E /* OnboardingView+Landing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */; }; + 9F9F325A2CEFA75100211B49 /* SettingsViewModel+ThreatDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F32592CEFA74600211B49 /* SettingsViewModel+ThreatDetection.swift */; }; 9FA5E44B2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */; }; 9FB027122C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */; }; 9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027132C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift */; }; @@ -2698,6 +2699,7 @@ 9F9A92332C86B42B001D036D /* AppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPicker.swift; sourceTree = ""; }; 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFirePixelMock.swift; sourceTree = ""; }; 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+Landing.swift"; sourceTree = ""; }; + 9F9F32592CEFA74600211B49 /* SettingsViewModel+ThreatDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsViewModel+ThreatDetection.swift"; sourceTree = ""; }; 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerViewFactory.swift; sourceTree = ""; }; 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+IntroDialogContent.swift"; sourceTree = ""; }; 9FB027132C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+BrowsersComparisonContent.swift"; sourceTree = ""; }; @@ -5935,6 +5937,7 @@ children = ( D6E83C5F2B22B3C9006C8AFB /* SettingsState.swift */, D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */, + 9F9F32592CEFA74600211B49 /* SettingsViewModel+ThreatDetection.swift */, D6E83C552B21ECC1006C8AFB /* SettingsLegacyViewProvider.swift */, 85449EFC23FDA71F00512AAF /* KeyboardSettings.swift */, 4B53648926718D0E001AA041 /* EmailWaitlist.swift */, @@ -8223,6 +8226,7 @@ EEC02C142B0519DE0045CE11 /* NetworkProtectionVPNLocationViewModel.swift in Sources */, D63FF8962C1B67E9006DE24D /* YoutubeOverlayUserScript.swift in Sources */, 9F38A28C2D09BDE500EB100E /* SpecialErrorPageThreatProvider.swift in Sources */, + 9F9F325A2CEFA75100211B49 /* SettingsViewModel+ThreatDetection.swift in Sources */, F13B4BC01F180D8A00814661 /* TabsModel.swift in Sources */, 8598D2E02CEB98B500C45685 /* Favicons.swift in Sources */, 8598D2E12CEB98B500C45685 /* NotFoundCachingDownloader.swift in Sources */, diff --git a/DuckDuckGo/SettingsGeneralView.swift b/DuckDuckGo/SettingsGeneralView.swift index 115c88a0fb..fd62d911a0 100644 --- a/DuckDuckGo/SettingsGeneralView.swift +++ b/DuckDuckGo/SettingsGeneralView.swift @@ -85,6 +85,19 @@ struct SettingsGeneralView: View { SettingsCellView(label: UserText.settingsAssociatedApps, accessory: .toggle(isOn: viewModel.universalLinksBinding)) } + + if viewModel.threatDetectionSettingsViewModel.shouldShowMaliciousSiteProtectionSection { + Section( + header: Text(verbatim: "Malicious Site Protection"), + footer: Text(verbatim: "Disabling this feature can put your personal information at risk. ") + .foregroundColor(.red) + ) { + SettingsCellView(label: "Warn me when a webpage may be malicious or fraudulent", + accessory: .toggle(isOn: viewModel.threatDetectionSettingsViewModel.threatDetectionBinding)) + } + } + + } .applySettingsListModifiers(title: UserText.general, displayMode: .inline, diff --git a/DuckDuckGo/SettingsViewModel+ThreatDetection.swift b/DuckDuckGo/SettingsViewModel+ThreatDetection.swift new file mode 100644 index 0000000000..9f512ce903 --- /dev/null +++ b/DuckDuckGo/SettingsViewModel+ThreatDetection.swift @@ -0,0 +1,80 @@ +// +// SettingsViewModel+ThreatDetection.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 Combine +import Core +import SwiftUI + +final class ThreatDetectionSettingsViewModel: ObservableObject { + @Published var shouldShowMaliciousSiteProtectionSection: Bool = false + + var threatDetectionBinding: Binding { + Binding( + get: { + self.manager.isEnabled + }, + set: { + self.manager.isEnabled = $0 + } + ) + } + + private let manager: ThreatDetetctionPreferencesManaging + private let featureChecker: ThreatDetectionSettingsChecking + + init( + manager: ThreatDetetctionPreferencesManaging = ThreatDetectionPreferencesManager(), + featureChecker: ThreatDetectionSettingsChecking = ThreatDetectionFeatureCheck() + ) { + self.manager = manager + self.featureChecker = featureChecker + shouldShowMaliciousSiteProtectionSection = featureChecker.isThreatDetectionSettingsEnabled + } +} + +protocol ThreatDetectionPreferencesStorage: AnyObject { + var isEnabled: Bool { get set } +} + +final class ThreatDetectionPreferencesUserDefaultsStore: ThreatDetectionPreferencesStorage { + @UserDefaultsWrapper(key: .threatDetectionEnabled, defaultValue: false) + var isEnabled: Bool +} + +protocol ThreatDetetctionPreferencesManaging: AnyObject { + var isEnabled: Bool { get set } +} + +final class ThreatDetectionPreferencesManager: ThreatDetetctionPreferencesManaging { + + @Published var isEnabled: Bool { + didSet { + store.isEnabled = isEnabled + print("~~~IS ENABLED: ", isEnabled) + } + } + + private let store: ThreatDetectionPreferencesStorage + + init(store: ThreatDetectionPreferencesStorage = ThreatDetectionPreferencesUserDefaultsStore()) { + self.store = store + self.isEnabled = store.isEnabled + } +} diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index cfdc9ae43c..796e318c25 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -46,6 +46,7 @@ final class SettingsViewModel: ObservableObject { let privacyProDataReporter: PrivacyProDataReporting? let textZoomCoordinator: TextZoomCoordinating let aiChatSettings: AIChatSettingsProvider + @Published var threatDetectionSettingsViewModel: ThreatDetectionSettingsViewModel = ThreatDetectionSettingsViewModel() // Subscription Dependencies let subscriptionManager: SubscriptionManager From c4e7bc9a788334d356c6cef78931da2bc9192148 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 16 Dec 2024 15:40:03 +0100 Subject: [PATCH 2/6] Add Malicious Site Protection Learn More URL --- Core/AppURLs.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/AppURLs.swift b/Core/AppURLs.swift index 56542c2066..b7e28f68d3 100644 --- a/Core/AppURLs.swift +++ b/Core/AppURLs.swift @@ -38,6 +38,7 @@ public extension URL { static let apps = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/apps?origin=funnel_app_ios"))! static let searchSettings = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/settings"))! static let autofillHelpPageLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/sync-and-backup/password-manager-security/"))! + static let maliciousSiteProtectionLearnMore = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/privacy/phishing-and-malware-protection/"))! static let surrogates = URL(string: "\(staticBase)/surrogates.txt")! From e76b5855422bec17c9fcdf088bbdf70ad0bf9257 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 16 Dec 2024 16:03:43 +0100 Subject: [PATCH 3/6] Add tests for Preferences Manager --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + ...iousSiteProtectionPreferencesManager.swift | 25 ++++++- ...iteProtectionPreferencesManagerTests.swift | 74 +++++++++++++++++++ 3 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionPreferencesManagerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index aac8eeee32..1ca25e0cdd 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -752,6 +752,7 @@ 9F06EB7D2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB7C2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift */; }; 9F06EB822D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB802D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift */; }; 9F06EB872D0C733B00905426 /* MaliciousSiteProtectionPreferencesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB862D0C733900905426 /* MaliciousSiteProtectionPreferencesManager.swift */; }; + 9F06EB922D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB912D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift */; }; 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; }; 9F1798572CD2443F0073018B /* AddToDockPromoViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */; }; 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */; }; @@ -2634,6 +2635,7 @@ 9F06EB7C2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionManagerTests.swift; sourceTree = ""; }; 9F06EB802D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionMocks.swift; sourceTree = ""; }; 9F06EB862D0C733900905426 /* MaliciousSiteProtectionPreferencesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionPreferencesManager.swift; sourceTree = ""; }; + 9F06EB912D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionPreferencesManagerTests.swift; sourceTree = ""; }; 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; }; 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToDockPromoViewModelTests.swift; sourceTree = ""; }; 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackground.swift; sourceTree = ""; }; @@ -5069,6 +5071,7 @@ 9F06EB7F2D0AEE0600905426 /* Mocks */, 9F06EB792D09EC2000905426 /* MaliciousSiteProtectionFeatureFlagsTests.swift */, 9F06EB7C2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift */, + 9F06EB912D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift */, ); path = MaliciousSiteProtection; sourceTree = ""; @@ -8702,6 +8705,7 @@ 85E065C12C73ADDD00D73E2A /* UsageSegmentationStorageTests.swift in Sources */, 8536A1CA209AF6490050739E /* HomeRowReminderTests.swift in Sources */, 851DFD8A212C5EE800D95F20 /* TabSwitcherButtonTests.swift in Sources */, + 9F06EB922D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift in Sources */, 98983096255B5019003339A2 /* BookmarksCachingSearchTests.swift in Sources */, D6B67A122C332B6E002122EB /* DuckPlayerMocks.swift in Sources */, 9FEA22352C327226006B03BF /* MockTimer.swift in Sources */, diff --git a/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift b/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift index 49f1321268..05be25310d 100644 --- a/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift +++ b/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift @@ -19,22 +19,39 @@ import Foundation import Combine +import Core + +protocol MaliciousSiteProtectionPreferencesStorage: AnyObject { + var isEnabled: Bool { get set } +} + +final class MaliciousSiteProtectionPreferencesUserDefaultsStore: MaliciousSiteProtectionPreferencesStorage { + @UserDefaultsWrapper(key: .maliciousSiteProtectionEnabled, defaultValue: false) + var isEnabled: Bool +} protocol MaliciousSiteProtectionPreferencesPublishing { var isEnabled: Bool { get } var isEnabledPublisher: AnyPublisher { get } } -protocol MaliciousSiteProtectionPreferencesManaging { +protocol MaliciousSiteProtectionPreferencesManaging: AnyObject { var isEnabled: Bool { get set } } final class MaliciousSiteProtectionPreferencesManager: MaliciousSiteProtectionPreferencesManaging, MaliciousSiteProtectionPreferencesPublishing { - @Published var isEnabled: Bool + @Published var isEnabled: Bool { + didSet { + store.isEnabled = isEnabled + } + } var isEnabledPublisher: AnyPublisher { $isEnabled.eraseToAnyPublisher() } - init() { - isEnabled = true + private let store: MaliciousSiteProtectionPreferencesStorage + + init(store: MaliciousSiteProtectionPreferencesStorage = MaliciousSiteProtectionPreferencesUserDefaultsStore()) { + self.store = store + isEnabled = store.isEnabled } } diff --git a/DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionPreferencesManagerTests.swift b/DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionPreferencesManagerTests.swift new file mode 100644 index 0000000000..093c248f75 --- /dev/null +++ b/DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionPreferencesManagerTests.swift @@ -0,0 +1,74 @@ +// +// MaliciousSiteProtectionPreferencesManagerTests.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 Testing +import Combine +@testable import DuckDuckGo + +final class MaliciousSiteProtectionPreferencesManagerTests { + private var sut: MaliciousSiteProtectionPreferencesManager! + private var store: MockMaliciousSiteProtectionPreferencesStore! + private var cancellables: Set! + + init() { + cancellables = [] + store = MockMaliciousSiteProtectionPreferencesStore() + sut = MaliciousSiteProtectionPreferencesManager(store: store) + } + + @Test( + "Update Malicious Site Protection Storage", + arguments: [ + true, + false + ] + ) + func whenIsEnabledIsSet_ThenStoreIsUpdated(value: Bool) { + // GIVEN + store.isEnabled = !value + + // WHEN + sut.isEnabled = value + + // THEN + #expect(store.isEnabled == value) + } + + @Test( + "Publish Malicious Site Protection User Preferences", + arguments: [ + true, + false + ] + ) + func whenIsEnabledIsSet_ThenValueIsPublished(value: Bool) { + // GIVEN + var capturedIsEnabled: Bool? + sut.isEnabledPublisher.sink { isEnabled in + capturedIsEnabled = isEnabled + } + .store(in: &cancellables) + + // WHEN + sut.isEnabled = value + + // THEN + #expect(capturedIsEnabled == value) + } +} From 06ed80bf7dd1fae71763c0fc9085d52499706cb3 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 16 Dec 2024 16:05:40 +0100 Subject: [PATCH 4/6] Add Settings VM and tests --- DuckDuckGo.xcodeproj/project.pbxproj | 22 ++- ...ciousSiteProtectionSettingsViewModel.swift | 60 +++++++ .../SettingsViewModel+ThreatDetection.swift | 80 --------- DuckDuckGo/SettingsViewModel.swift | 1 - ...SiteProtectionSettingsViewModelTests.swift | 165 ++++++++++++++++++ .../Mocks/MaliciousSiteProtectionMocks.swift | 5 + .../MockMaliciousSiteProtectionManager.swift | 0 7 files changed, 247 insertions(+), 86 deletions(-) create mode 100644 DuckDuckGo/MaliciousSiteProtection/Settings/MaliciousSiteProtectionSettingsViewModel.swift delete mode 100644 DuckDuckGo/SettingsViewModel+ThreatDetection.swift create mode 100644 DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionSettingsViewModelTests.swift rename DuckDuckGoTests/{SpecialErrorPage/TestDoubles => MaliciousSiteProtection/Mocks}/MockMaliciousSiteProtectionManager.swift (100%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1ca25e0cdd..a0d8294d2d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -752,6 +752,7 @@ 9F06EB7D2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB7C2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift */; }; 9F06EB822D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB802D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift */; }; 9F06EB872D0C733B00905426 /* MaliciousSiteProtectionPreferencesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB862D0C733900905426 /* MaliciousSiteProtectionPreferencesManager.swift */; }; + 9F06EB8C2D10578000905426 /* MaliciousSiteProtectionSettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB8B2D10578000905426 /* MaliciousSiteProtectionSettingsViewModelTests.swift */; }; 9F06EB922D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB912D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift */; }; 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; }; 9F1798572CD2443F0073018B /* AddToDockPromoViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */; }; @@ -821,7 +822,7 @@ 9F9A92342C86B42B001D036D /* AppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A92332C86B42B001D036D /* AppIconPicker.swift */; }; 9F9EE4CE2C377D4900D4118E /* OnboardingFirePixelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */; }; 9F9EE4D42C37BB1300D4118E /* OnboardingView+Landing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */; }; - 9F9F325A2CEFA75100211B49 /* SettingsViewModel+ThreatDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F32592CEFA74600211B49 /* SettingsViewModel+ThreatDetection.swift */; }; + 9F9F325A2CEFA75100211B49 /* MaliciousSiteProtectionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F32592CEFA74600211B49 /* MaliciousSiteProtectionSettingsViewModel.swift */; }; 9FA5E44B2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */; }; 9FB027122C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */; }; 9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027132C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift */; }; @@ -2635,6 +2636,7 @@ 9F06EB7C2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionManagerTests.swift; sourceTree = ""; }; 9F06EB802D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionMocks.swift; sourceTree = ""; }; 9F06EB862D0C733900905426 /* MaliciousSiteProtectionPreferencesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionPreferencesManager.swift; sourceTree = ""; }; + 9F06EB8B2D10578000905426 /* MaliciousSiteProtectionSettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionSettingsViewModelTests.swift; sourceTree = ""; }; 9F06EB912D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionPreferencesManagerTests.swift; sourceTree = ""; }; 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; }; 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToDockPromoViewModelTests.swift; sourceTree = ""; }; @@ -2701,7 +2703,7 @@ 9F9A92332C86B42B001D036D /* AppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPicker.swift; sourceTree = ""; }; 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFirePixelMock.swift; sourceTree = ""; }; 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+Landing.swift"; sourceTree = ""; }; - 9F9F32592CEFA74600211B49 /* SettingsViewModel+ThreatDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsViewModel+ThreatDetection.swift"; sourceTree = ""; }; + 9F9F32592CEFA74600211B49 /* MaliciousSiteProtectionSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionSettingsViewModel.swift; sourceTree = ""; }; 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerViewFactory.swift; sourceTree = ""; }; 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+IntroDialogContent.swift"; sourceTree = ""; }; 9FB027132C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+BrowsersComparisonContent.swift"; sourceTree = ""; }; @@ -5071,6 +5073,7 @@ 9F06EB7F2D0AEE0600905426 /* Mocks */, 9F06EB792D09EC2000905426 /* MaliciousSiteProtectionFeatureFlagsTests.swift */, 9F06EB7C2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift */, + 9F06EB8B2D10578000905426 /* MaliciousSiteProtectionSettingsViewModelTests.swift */, 9F06EB912D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift */, ); path = MaliciousSiteProtection; @@ -5079,6 +5082,7 @@ 9F06EB7F2D0AEE0600905426 /* Mocks */ = { isa = PBXGroup; children = ( + 9FBC76692CFE3802008B21E7 /* MockMaliciousSiteProtectionManager.swift */, 9F06EB802D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift */, ); path = Mocks; @@ -5092,6 +5096,14 @@ path = UserPreferences; sourceTree = ""; }; + 9F06EB882D0D737500905426 /* Settings */ = { + isa = PBXGroup; + children = ( + 9F9F32592CEFA74600211B49 /* MaliciousSiteProtectionSettingsViewModel.swift */, + ); + path = Settings; + sourceTree = ""; + }; 9F23B7FF2C2BABE000950875 /* OnboardingIntro */ = { isa = PBXGroup; children = ( @@ -5146,6 +5158,7 @@ 9F254AA92CF47CD30063B308 /* MaliciousSiteProtection */ = { isa = PBXGroup; children = ( + 9F06EB882D0D737500905426 /* Settings */, 9F06EB852D0C733100905426 /* UserPreferences */, 9F06EB742D09E8D200905426 /* FeatureFlags */, 9F254AF22CF8E1F30063B308 /* Resources */, @@ -5176,7 +5189,6 @@ 9F254AD72CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift */, 9F254ADA2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift */, 9F254ADD2CF636CF0063B308 /* DummyWKNavigation.swift */, - 9FBC76692CFE3802008B21E7 /* MockMaliciousSiteProtectionManager.swift */, ); path = TestDoubles; sourceTree = ""; @@ -5940,7 +5952,6 @@ children = ( D6E83C5F2B22B3C9006C8AFB /* SettingsState.swift */, D6E83C2D2B1EA06E006C8AFB /* SettingsViewModel.swift */, - 9F9F32592CEFA74600211B49 /* SettingsViewModel+ThreatDetection.swift */, D6E83C552B21ECC1006C8AFB /* SettingsLegacyViewProvider.swift */, 85449EFC23FDA71F00512AAF /* KeyboardSettings.swift */, 4B53648926718D0E001AA041 /* EmailWaitlist.swift */, @@ -8229,7 +8240,7 @@ EEC02C142B0519DE0045CE11 /* NetworkProtectionVPNLocationViewModel.swift in Sources */, D63FF8962C1B67E9006DE24D /* YoutubeOverlayUserScript.swift in Sources */, 9F38A28C2D09BDE500EB100E /* SpecialErrorPageThreatProvider.swift in Sources */, - 9F9F325A2CEFA75100211B49 /* SettingsViewModel+ThreatDetection.swift in Sources */, + 9F9F325A2CEFA75100211B49 /* MaliciousSiteProtectionSettingsViewModel.swift in Sources */, F13B4BC01F180D8A00814661 /* TabsModel.swift in Sources */, 8598D2E02CEB98B500C45685 /* Favicons.swift in Sources */, 8598D2E12CEB98B500C45685 /* NotFoundCachingDownloader.swift in Sources */, @@ -8598,6 +8609,7 @@ 987130C5294AAB9F00AB05E0 /* BookmarkEditorViewModelTests.swift in Sources */, BDFF03262BA3DA4900F324C9 /* NetworkProtectionFeatureVisibilityTests.swift in Sources */, 9F8E0F332CCA642D001EA7C5 /* VideoPlayerViewModelTests.swift in Sources */, + 9F06EB8C2D10578000905426 /* MaliciousSiteProtectionSettingsViewModelTests.swift in Sources */, D62EC3BA2C246A7000FC9D04 /* YoutublePlayerNavigationHandlerTests.swift in Sources */, 1EAABE712C99FC75003F5137 /* SubscriptionFeatureAvailabilityMock.swift in Sources */, 8341D807212D5E8D000514C2 /* HashExtensionTest.swift in Sources */, diff --git a/DuckDuckGo/MaliciousSiteProtection/Settings/MaliciousSiteProtectionSettingsViewModel.swift b/DuckDuckGo/MaliciousSiteProtection/Settings/MaliciousSiteProtectionSettingsViewModel.swift new file mode 100644 index 0000000000..30a2bb64db --- /dev/null +++ b/DuckDuckGo/MaliciousSiteProtection/Settings/MaliciousSiteProtectionSettingsViewModel.swift @@ -0,0 +1,60 @@ +// +// MaliciousSiteProtectionSettingsViewModel.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 Combine +import Core +import SwiftUI + +final class MaliciousSiteProtectionSettingsViewModel: ObservableObject { + @Published var shouldShowMaliciousSiteProtectionSection = false + @Published var isMaliciousSiteProtectionEnabled: Bool = false + + var maliciousSiteProtectionBinding: Binding { + Binding( + get: { + self.manager.isEnabled + }, + set: { + self.manager.isEnabled = $0 + self.isMaliciousSiteProtectionEnabled = $0 + } + ) + } + + private let manager: MaliciousSiteProtectionPreferencesManaging + private let featureFlagger: MaliciousSiteProtectionFeatureFlagger + private let urlOpener: URLOpener + + init( + manager: MaliciousSiteProtectionPreferencesManaging = MaliciousSiteProtectionPreferencesManager(), + featureFlagger: MaliciousSiteProtectionFeatureFlagger = MaliciousSiteProtectionFeatureFlags(), + urlOpener: URLOpener = UIApplication.shared + ) { + self.manager = manager + self.featureFlagger = featureFlagger + self.urlOpener = urlOpener + shouldShowMaliciousSiteProtectionSection = true //featureFlagger.isMaliciousSiteProtectionEnabled + isMaliciousSiteProtectionEnabled = manager.isEnabled + } + + func learnMoreAction() { + urlOpener.open(URL.maliciousSiteProtectionLearnMore) + } +} diff --git a/DuckDuckGo/SettingsViewModel+ThreatDetection.swift b/DuckDuckGo/SettingsViewModel+ThreatDetection.swift deleted file mode 100644 index 9f512ce903..0000000000 --- a/DuckDuckGo/SettingsViewModel+ThreatDetection.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// SettingsViewModel+ThreatDetection.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 Combine -import Core -import SwiftUI - -final class ThreatDetectionSettingsViewModel: ObservableObject { - @Published var shouldShowMaliciousSiteProtectionSection: Bool = false - - var threatDetectionBinding: Binding { - Binding( - get: { - self.manager.isEnabled - }, - set: { - self.manager.isEnabled = $0 - } - ) - } - - private let manager: ThreatDetetctionPreferencesManaging - private let featureChecker: ThreatDetectionSettingsChecking - - init( - manager: ThreatDetetctionPreferencesManaging = ThreatDetectionPreferencesManager(), - featureChecker: ThreatDetectionSettingsChecking = ThreatDetectionFeatureCheck() - ) { - self.manager = manager - self.featureChecker = featureChecker - shouldShowMaliciousSiteProtectionSection = featureChecker.isThreatDetectionSettingsEnabled - } -} - -protocol ThreatDetectionPreferencesStorage: AnyObject { - var isEnabled: Bool { get set } -} - -final class ThreatDetectionPreferencesUserDefaultsStore: ThreatDetectionPreferencesStorage { - @UserDefaultsWrapper(key: .threatDetectionEnabled, defaultValue: false) - var isEnabled: Bool -} - -protocol ThreatDetetctionPreferencesManaging: AnyObject { - var isEnabled: Bool { get set } -} - -final class ThreatDetectionPreferencesManager: ThreatDetetctionPreferencesManaging { - - @Published var isEnabled: Bool { - didSet { - store.isEnabled = isEnabled - print("~~~IS ENABLED: ", isEnabled) - } - } - - private let store: ThreatDetectionPreferencesStorage - - init(store: ThreatDetectionPreferencesStorage = ThreatDetectionPreferencesUserDefaultsStore()) { - self.store = store - self.isEnabled = store.isEnabled - } -} diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 796e318c25..cfdc9ae43c 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -46,7 +46,6 @@ final class SettingsViewModel: ObservableObject { let privacyProDataReporter: PrivacyProDataReporting? let textZoomCoordinator: TextZoomCoordinating let aiChatSettings: AIChatSettingsProvider - @Published var threatDetectionSettingsViewModel: ThreatDetectionSettingsViewModel = ThreatDetectionSettingsViewModel() // Subscription Dependencies let subscriptionManager: SubscriptionManager diff --git a/DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionSettingsViewModelTests.swift b/DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionSettingsViewModelTests.swift new file mode 100644 index 0000000000..60a987b3a8 --- /dev/null +++ b/DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionSettingsViewModelTests.swift @@ -0,0 +1,165 @@ +// +// MaliciousSiteProtectionSettingsViewModelTests.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 Testing +@testable import DuckDuckGo + +@Suite("Malicious Site Protection - Settings View Model Unit Tests") +final class MaliciousSiteProtectionSettingsViewModelTests { + private var sut: MaliciousSiteProtectionSettingsViewModel! + private var preferencesManager: MockMaliciousSiteProtectionPreferencesManager! + private var featureFlagger: MockMaliciousSiteProtectionFeatureFlags! + private var urlOpener: MockURLOpener! + + init() { + preferencesManager = MockMaliciousSiteProtectionPreferencesManager() + featureFlagger = MockMaliciousSiteProtectionFeatureFlags() + urlOpener = MockURLOpener() + setupSUT() + } + + @Test("Malicious Site Protection Settings Section should be shown") + func whenInit_AndIsMaliciousSiteProtectionSetToTrue_ThenShouldShowMaliciousSiteProtectionSectionReturnsTrue() { + // GIVEN + featureFlagger.isMaliciousSiteProtectionEnabled = true + setupSUT() + + // WHEN + let result = sut.shouldShowMaliciousSiteProtectionSection + + // THEN + #expect(result) + } + + @Test("Malicious Site Protection Settings Section should not be shown") + func whenInit_AndIsMaliciousSiteProtectionSetToFalse_ThenShouldShowMaliciousSiteProtectionSectionReturnsFalse() { + // GIVEN + featureFlagger.isMaliciousSiteProtectionEnabled = false + setupSUT() + + // WHEN + let result = sut.shouldShowMaliciousSiteProtectionSection + + // THEN + #expect(!result) + } + + @Test("Malicious Site Protection preference is enabled") + func whenInit_AndIsEnabledPreferenceSetToTrue_ThenIsMaliciousSiteProtectionEnabledReturnsTrue() { + // GIVEN + preferencesManager.isEnabled = true + setupSUT() + + // WHEN + let result = sut.isMaliciousSiteProtectionEnabled + + // THEN + #expect(result) + } + + @Test("Malicious Site Protection preference is disabled") + func whenInit_AndIsEnabledPreferenceSetToFalse_ThenIsMaliciousSiteProtectionEnabledReturnsFalse() { + // GIVEN + preferencesManager.isEnabled = false + setupSUT() + + // WHEN + let result = sut.isMaliciousSiteProtectionEnabled + + // THEN + #expect(!result) + } + + @Test("Malicious Site Protection Settings binding value is true") + func whenMaliciousSiteProtectionBindingIsCalled_AndValueIsTrue_ThenReturnTrue() { + // GIVEN + preferencesManager.isEnabled = true + + // WHEN + let result = sut.maliciousSiteProtectionBinding + + // THEN + #expect(result.wrappedValue) + } + + @Test("Malicious Site Protection Settings binding value is false") + func whenMaliciousSiteProtectionBindingIsCalled_AndValueIsFalse_ThenReturnFalse() { + // GIVEN + preferencesManager.isEnabled = false + + // WHEN + let result = sut.maliciousSiteProtectionBinding + + // THEN + #expect(!result.wrappedValue) + } + + @Test("Malicious Site Protection Settings binding value is set to true") + func whenMaliciousSiteProtectionBindingIsSetToTrue_ThenIsMaliciousSiteProtectionEnabledIsSetToTrue() { + // GIVEN + preferencesManager.isEnabled = false + #expect(!preferencesManager.isEnabled) + + // WHEN + sut.maliciousSiteProtectionBinding.wrappedValue = true + + // THEN + #expect(preferencesManager.isEnabled) + } + + @Test("Malicious Site Protection Settings binding value is set to false") + func whenMaliciousSiteProtectionBindingIsSetToFalse_ThenIsMaliciousSiteProtectionEnabledIsSetToFalse() { + // GIVEN + preferencesManager.isEnabled = true + #expect(preferencesManager.isEnabled) + + // WHEN + sut.maliciousSiteProtectionBinding.wrappedValue = false + + // THEN + #expect(!preferencesManager.isEnabled) + } + + @Test("Open Malicious Site Protection Learn More") + func whenLearnMoreAction_ThenShouldNavigateToLearnMorePage() { + // GIVEN + #expect(!urlOpener.didCallOpenURL) + #expect(urlOpener.capturedURL == nil) + setupSUT() + + // WHEN + sut.learnMoreAction() + + // THEN + #expect(urlOpener.didCallOpenURL) + #expect(urlOpener.capturedURL == .maliciousSiteProtectionLearnMore) + } +} + +extension MaliciousSiteProtectionSettingsViewModelTests { + + func setupSUT() { + sut = MaliciousSiteProtectionSettingsViewModel( + manager: preferencesManager, + featureFlagger: featureFlagger, + urlOpener: urlOpener + ) + } + +} diff --git a/DuckDuckGoTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift b/DuckDuckGoTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift index 0944c13579..e5b7dc35c1 100644 --- a/DuckDuckGoTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift +++ b/DuckDuckGoTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift @@ -125,3 +125,8 @@ final class MockMaliciousSiteProtectionFeatureFlags: MaliciousSiteProtectionFeat } } + +final class MockMaliciousSiteProtectionPreferencesStore: MaliciousSiteProtectionPreferencesStorage { + var isEnabled: Bool = true + +} diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockMaliciousSiteProtectionManager.swift b/DuckDuckGoTests/MaliciousSiteProtection/Mocks/MockMaliciousSiteProtectionManager.swift similarity index 100% rename from DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockMaliciousSiteProtectionManager.swift rename to DuckDuckGoTests/MaliciousSiteProtection/Mocks/MockMaliciousSiteProtectionManager.swift From 7efb9af86b2ede1c02cf006a70a883835fc94a77 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 16 Dec 2024 16:06:03 +0100 Subject: [PATCH 5/6] Add View for settings --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++ DuckDuckGo/SettingsGeneralView.swift | 12 +--- .../SettingsMaliciousSiteProtectionView.swift | 56 +++++++++++++++++++ DuckDuckGo/UserText.swift | 7 +++ DuckDuckGo/en.lproj/Localizable.strings | 12 ++++ 5 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 DuckDuckGo/SettingsMaliciousSiteProtectionView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a0d8294d2d..2ad1cd6b98 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -752,6 +752,7 @@ 9F06EB7D2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB7C2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift */; }; 9F06EB822D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB802D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift */; }; 9F06EB872D0C733B00905426 /* MaliciousSiteProtectionPreferencesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB862D0C733900905426 /* MaliciousSiteProtectionPreferencesManager.swift */; }; + 9F06EB8A2D10560200905426 /* SettingsMaliciousSiteProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB892D10560200905426 /* SettingsMaliciousSiteProtectionView.swift */; }; 9F06EB8C2D10578000905426 /* MaliciousSiteProtectionSettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB8B2D10578000905426 /* MaliciousSiteProtectionSettingsViewModelTests.swift */; }; 9F06EB922D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB912D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift */; }; 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; }; @@ -2636,6 +2637,7 @@ 9F06EB7C2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionManagerTests.swift; sourceTree = ""; }; 9F06EB802D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionMocks.swift; sourceTree = ""; }; 9F06EB862D0C733900905426 /* MaliciousSiteProtectionPreferencesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionPreferencesManager.swift; sourceTree = ""; }; + 9F06EB892D10560200905426 /* SettingsMaliciousSiteProtectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMaliciousSiteProtectionView.swift; sourceTree = ""; }; 9F06EB8B2D10578000905426 /* MaliciousSiteProtectionSettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionSettingsViewModelTests.swift; sourceTree = ""; }; 9F06EB912D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionPreferencesManagerTests.swift; sourceTree = ""; }; 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; }; @@ -3481,6 +3483,7 @@ 1DEAADED2BA45DFE00E25A97 /* SettingsDataClearingView.swift */, D65625A02C232F5E006EF297 /* SettingsDuckPlayerView.swift */, 317CA3422CFF82DB00F88848 /* SettingsAIChatView.swift */, + 9F06EB892D10560200905426 /* SettingsMaliciousSiteProtectionView.swift */, ); name = MainSettings; sourceTree = ""; @@ -8413,6 +8416,7 @@ 9FCFCD852C75C91A006EB7A0 /* ProgressBarView.swift in Sources */, 6F3537A42C4AC140009F8717 /* NewTabPageDaxLogoView.swift in Sources */, 314C92B827C3DD660042EC96 /* QuickLookPreviewView.swift in Sources */, + 9F06EB8A2D10560200905426 /* SettingsMaliciousSiteProtectionView.swift in Sources */, 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */, F1AE54E81F0425FC00D9A700 /* AuthenticationViewController.swift in Sources */, 560E990F2BEE2CB800507CE0 /* SyncErrorMessage.swift in Sources */, diff --git a/DuckDuckGo/SettingsGeneralView.swift b/DuckDuckGo/SettingsGeneralView.swift index fd62d911a0..2e63dd124c 100644 --- a/DuckDuckGo/SettingsGeneralView.swift +++ b/DuckDuckGo/SettingsGeneralView.swift @@ -86,17 +86,7 @@ struct SettingsGeneralView: View { accessory: .toggle(isOn: viewModel.universalLinksBinding)) } - if viewModel.threatDetectionSettingsViewModel.shouldShowMaliciousSiteProtectionSection { - Section( - header: Text(verbatim: "Malicious Site Protection"), - footer: Text(verbatim: "Disabling this feature can put your personal information at risk. ") - .foregroundColor(.red) - ) { - SettingsCellView(label: "Warn me when a webpage may be malicious or fraudulent", - accessory: .toggle(isOn: viewModel.threatDetectionSettingsViewModel.threatDetectionBinding)) - } - } - + SettingsMaliciousProtectionView() } .applySettingsListModifiers(title: UserText.general, diff --git a/DuckDuckGo/SettingsMaliciousSiteProtectionView.swift b/DuckDuckGo/SettingsMaliciousSiteProtectionView.swift new file mode 100644 index 0000000000..2f7d0060f2 --- /dev/null +++ b/DuckDuckGo/SettingsMaliciousSiteProtectionView.swift @@ -0,0 +1,56 @@ +// +// SettingsMaliciousSiteProtectionView.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 SwiftUI +import DuckUI + +struct SettingsMaliciousProtectionView: View { + @StateObject private var maliciousSiteProtectionSettingsModel = MaliciousSiteProtectionSettingsViewModel() + + var body: some View { + if maliciousSiteProtectionSettingsModel.shouldShowMaliciousSiteProtectionSection { + Section( + header: Text(UserText.MaliciousSiteProtectionSettings.header), + footer: + VStack(alignment: .leading, spacing: 10) { + Button(action: maliciousSiteProtectionSettingsModel.learnMoreAction) { + Text(UserText.MaliciousSiteProtectionSettings.footerLearnMore) + .foregroundColor(.blueBase) + } + + Text(UserText.MaliciousSiteProtectionSettings.footerDisabledMessage) + .opacity(maliciousSiteProtectionSettingsModel.maliciousSiteProtectionBinding.wrappedValue ? 0 : 1) + .foregroundColor(.red) + .font(.footnote) + } + ) { + SettingsCellView( + label: UserText.MaliciousSiteProtectionSettings.toggleMessage, + accessory: .toggle(isOn: maliciousSiteProtectionSettingsModel.maliciousSiteProtectionBinding) + ) + } + } else { + EmptyView() + } + } +} + +#Preview { + SettingsMaliciousProtectionView() +} diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 86a2337975..e496cd3956 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1364,6 +1364,13 @@ AI Chat is an optional feature available at [duck.ai](ddgquicklink://duck.ai) th public static let aiChatSettingsEnableAddressBarToggle = NSLocalizedString("aichat.settings.enable.address-bar-toggle", value: "Show AI Chat While Searching", comment: "Toggle text to enable/disable AI Chat in the address bar") + public enum MaliciousSiteProtectionSettings { + public static let header = NSLocalizedString("malicious-site-protection.settings.header", value: "Malicious Site Protection", comment: "Header text for Malicious Site Protection settings") + public static let toggleMessage = NSLocalizedString("malicious-site-protection.settings.toggle.message", value: "Warn me on sites flagged for phishing or malware", comment: "Text explaining what happens when Malicious Site Protection is enabled") + public static let footerLearnMore = NSLocalizedString("malicious-site-protection.settings.footer.button.learn-more", value: "Learn More", comment: "Button that redirect the user to a web page explaining what Malicious Site Protection is") + public static let footerDisabledMessage = NSLocalizedString("malicious-site-protection.settings.footer.message", value: "Disabling this feature can put your personal information at risk.", comment: "Footer text for Malicious Site Protection settings warning the user about the risks of disabling the feature") + } + // MARK: - New Tab Page // MARK: Shortcuts diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index d1bf6015ea..81060a5498 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1617,6 +1617,18 @@ https://duckduckgo.com/mac"; /* Title for the Mac Waitlist feature */ "mac-waitlist.title" = "DuckDuckGo App for Mac"; +/* Button that redirect the user to a web page explaining what Malicious Site Protection is */ +"malicious-site-protection.settings.footer.button.learn-more" = "Learn More"; + +/* Footer text for Malicious Site Protection settings warning the user about the risks of disabling the feature */ +"malicious-site-protection.settings.footer.message" = "Disabling this feature can put your personal information at risk."; + +/* Header text for Malicious Site Protection settings */ +"malicious-site-protection.settings.header" = "Malicious Site Protection"; + +/* Text explaining what happens when Malicious Site Protection is enabled */ +"malicious-site-protection.settings.toggle.message" = "Warn me on sites flagged for phishing or malware"; + /* No comment provided by engineer. */ "menu.button.hint" = "Browsing Menu"; From 74e93537540208f73b9eafd252405eaa1a826afd Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 19 Dec 2024 12:11:23 +0100 Subject: [PATCH 6/6] Fix malicious site protection user preference being out of sync --- DuckDuckGo/AppDependencyProvider.swift | 2 ++ .../MaliciousSiteProtectionManager.swift | 2 +- .../MaliciousSiteProtectionSettingsViewModel.swift | 4 ++-- .../MaliciousSiteProtectionPreferencesManager.swift | 13 +++++++++---- .../Mocks/MaliciousSiteProtectionMocks.swift | 3 ++- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 00b6f9fc29..a54a0db8cf 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -52,6 +52,7 @@ protocol DependencyProvider { var serverInfoObserver: ConnectionServerInfoObserver { get } var vpnSettings: VPNSettings { get } var persistentPixel: PersistentPixelFiring { get } + var maliciousSiteProtectionPreferencesManager: MaliciousSiteProtectionPreferencesManaging { get } } @@ -91,6 +92,7 @@ final class AppDependencyProvider: DependencyProvider { let serverInfoObserver: ConnectionServerInfoObserver = ConnectionServerInfoObserverThroughSession() let vpnSettings = VPNSettings(defaults: .networkProtectionGroupDefaults) let persistentPixel: PersistentPixelFiring = PersistentPixel() + let maliciousSiteProtectionPreferencesManager: MaliciousSiteProtectionPreferencesManaging = MaliciousSiteProtectionPreferencesManager() private init() { let featureFlaggerOverrides = FeatureFlagLocalOverrides(keyValueStore: UserDefaults(suiteName: FeatureFlag.localOverrideStoreName)!, diff --git a/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift b/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift index c4f5fea1bc..e1658f866d 100644 --- a/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift +++ b/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift @@ -46,7 +46,7 @@ final class MaliciousSiteProtectionManager: MaliciousSiteDetecting { embeddedDataProvider: MaliciousSiteProtection.EmbeddedDataProviding = EmbeddedDataProvider(), dataManager: MaliciousSiteProtection.DataManager? = nil, detector: MaliciousSiteProtection.MaliciousSiteDetecting? = nil, - preferencesManager: MaliciousSiteProtectionPreferencesPublishing = MaliciousSiteProtectionPreferencesManager(), + preferencesManager: MaliciousSiteProtectionPreferencesPublishing = AppDependencyProvider.shared.maliciousSiteProtectionPreferencesManager, maliciousSiteProtectionFeatureFlagger: MaliciousSiteProtectionFeatureFlagger = MaliciousSiteProtectionFeatureFlags(), updateIntervalProvider: UpdateManager.UpdateIntervalProvider? = nil ) { diff --git a/DuckDuckGo/MaliciousSiteProtection/Settings/MaliciousSiteProtectionSettingsViewModel.swift b/DuckDuckGo/MaliciousSiteProtection/Settings/MaliciousSiteProtectionSettingsViewModel.swift index 30a2bb64db..c4e13afdd3 100644 --- a/DuckDuckGo/MaliciousSiteProtection/Settings/MaliciousSiteProtectionSettingsViewModel.swift +++ b/DuckDuckGo/MaliciousSiteProtection/Settings/MaliciousSiteProtectionSettingsViewModel.swift @@ -43,14 +43,14 @@ final class MaliciousSiteProtectionSettingsViewModel: ObservableObject { private let urlOpener: URLOpener init( - manager: MaliciousSiteProtectionPreferencesManaging = MaliciousSiteProtectionPreferencesManager(), + manager: MaliciousSiteProtectionPreferencesManaging = AppDependencyProvider.shared.maliciousSiteProtectionPreferencesManager, featureFlagger: MaliciousSiteProtectionFeatureFlagger = MaliciousSiteProtectionFeatureFlags(), urlOpener: URLOpener = UIApplication.shared ) { self.manager = manager self.featureFlagger = featureFlagger self.urlOpener = urlOpener - shouldShowMaliciousSiteProtectionSection = true //featureFlagger.isMaliciousSiteProtectionEnabled + shouldShowMaliciousSiteProtectionSection = featureFlagger.isMaliciousSiteProtectionEnabled isMaliciousSiteProtectionEnabled = manager.isEnabled } diff --git a/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift b/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift index 05be25310d..76d37dabf7 100644 --- a/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift +++ b/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift @@ -30,16 +30,21 @@ final class MaliciousSiteProtectionPreferencesUserDefaultsStore: MaliciousSitePr var isEnabled: Bool } -protocol MaliciousSiteProtectionPreferencesPublishing { +protocol MaliciousSiteProtectionPreferencesReadable: AnyObject { var isEnabled: Bool { get } - var isEnabledPublisher: AnyPublisher { get } } -protocol MaliciousSiteProtectionPreferencesManaging: AnyObject { +protocol MaliciousSiteProtectionPreferencesWritable: AnyObject { var isEnabled: Bool { get set } } -final class MaliciousSiteProtectionPreferencesManager: MaliciousSiteProtectionPreferencesManaging, MaliciousSiteProtectionPreferencesPublishing { +protocol MaliciousSiteProtectionPreferencesPublishing: MaliciousSiteProtectionPreferencesReadable { + var isEnabledPublisher: AnyPublisher { get } +} + +typealias MaliciousSiteProtectionPreferencesManaging = MaliciousSiteProtectionPreferencesWritable & MaliciousSiteProtectionPreferencesPublishing + +final class MaliciousSiteProtectionPreferencesManager: MaliciousSiteProtectionPreferencesManaging { @Published var isEnabled: Bool { didSet { store.isEnabled = isEnabled diff --git a/DuckDuckGoTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift b/DuckDuckGoTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift index e5b7dc35c1..e0c39a769d 100644 --- a/DuckDuckGoTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift +++ b/DuckDuckGoTests/MaliciousSiteProtection/Mocks/MaliciousSiteProtectionMocks.swift @@ -105,13 +105,14 @@ final class MockMaliciousSiteDetector: MaliciousSiteProtection.MaliciousSiteDete } } -final class MockMaliciousSiteProtectionPreferencesManager: MaliciousSiteProtectionPreferencesManaging, MaliciousSiteProtectionPreferencesPublishing { +final class MockMaliciousSiteProtectionPreferencesManager: MaliciousSiteProtectionPreferencesManaging { @Published var isEnabled: Bool = false var isEnabledPublisher: AnyPublisher { $isEnabled.eraseToAnyPublisher() } + } final class MockMaliciousSiteProtectionFeatureFlags: MaliciousSiteProtectionFeatureFlagger {