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")! 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..2ad1cd6b98 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -752,6 +752,9 @@ 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 */; }; 9F1798572CD2443F0073018B /* AddToDockPromoViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */; }; 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */; }; @@ -820,6 +823,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 /* 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 */; }; @@ -2633,6 +2637,9 @@ 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 = ""; }; 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToDockPromoViewModelTests.swift; sourceTree = ""; }; 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackground.swift; sourceTree = ""; }; @@ -2698,6 +2705,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 /* 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 = ""; }; @@ -3475,6 +3483,7 @@ 1DEAADED2BA45DFE00E25A97 /* SettingsDataClearingView.swift */, D65625A02C232F5E006EF297 /* SettingsDuckPlayerView.swift */, 317CA3422CFF82DB00F88848 /* SettingsAIChatView.swift */, + 9F06EB892D10560200905426 /* SettingsMaliciousSiteProtectionView.swift */, ); name = MainSettings; sourceTree = ""; @@ -5067,6 +5076,8 @@ 9F06EB7F2D0AEE0600905426 /* Mocks */, 9F06EB792D09EC2000905426 /* MaliciousSiteProtectionFeatureFlagsTests.swift */, 9F06EB7C2D0AEBD000905426 /* MaliciousSiteProtectionManagerTests.swift */, + 9F06EB8B2D10578000905426 /* MaliciousSiteProtectionSettingsViewModelTests.swift */, + 9F06EB912D10740500905426 /* MaliciousSiteProtectionPreferencesManagerTests.swift */, ); path = MaliciousSiteProtection; sourceTree = ""; @@ -5074,6 +5085,7 @@ 9F06EB7F2D0AEE0600905426 /* Mocks */ = { isa = PBXGroup; children = ( + 9FBC76692CFE3802008B21E7 /* MockMaliciousSiteProtectionManager.swift */, 9F06EB802D0AEE1F00905426 /* MaliciousSiteProtectionMocks.swift */, ); path = Mocks; @@ -5087,6 +5099,14 @@ path = UserPreferences; sourceTree = ""; }; + 9F06EB882D0D737500905426 /* Settings */ = { + isa = PBXGroup; + children = ( + 9F9F32592CEFA74600211B49 /* MaliciousSiteProtectionSettingsViewModel.swift */, + ); + path = Settings; + sourceTree = ""; + }; 9F23B7FF2C2BABE000950875 /* OnboardingIntro */ = { isa = PBXGroup; children = ( @@ -5141,6 +5161,7 @@ 9F254AA92CF47CD30063B308 /* MaliciousSiteProtection */ = { isa = PBXGroup; children = ( + 9F06EB882D0D737500905426 /* Settings */, 9F06EB852D0C733100905426 /* UserPreferences */, 9F06EB742D09E8D200905426 /* FeatureFlags */, 9F254AF22CF8E1F30063B308 /* Resources */, @@ -5171,7 +5192,6 @@ 9F254AD72CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift */, 9F254ADA2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift */, 9F254ADD2CF636CF0063B308 /* DummyWKNavigation.swift */, - 9FBC76692CFE3802008B21E7 /* MockMaliciousSiteProtectionManager.swift */, ); path = TestDoubles; sourceTree = ""; @@ -8223,6 +8243,7 @@ EEC02C142B0519DE0045CE11 /* NetworkProtectionVPNLocationViewModel.swift in Sources */, D63FF8962C1B67E9006DE24D /* YoutubeOverlayUserScript.swift in Sources */, 9F38A28C2D09BDE500EB100E /* SpecialErrorPageThreatProvider.swift in Sources */, + 9F9F325A2CEFA75100211B49 /* MaliciousSiteProtectionSettingsViewModel.swift in Sources */, F13B4BC01F180D8A00814661 /* TabsModel.swift in Sources */, 8598D2E02CEB98B500C45685 /* Favicons.swift in Sources */, 8598D2E12CEB98B500C45685 /* NotFoundCachingDownloader.swift in Sources */, @@ -8395,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 */, @@ -8591,6 +8613,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 */, @@ -8698,6 +8721,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/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 new file mode 100644 index 0000000000..c4e13afdd3 --- /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 = AppDependencyProvider.shared.maliciousSiteProtectionPreferencesManager, + featureFlagger: MaliciousSiteProtectionFeatureFlagger = MaliciousSiteProtectionFeatureFlags(), + urlOpener: URLOpener = UIApplication.shared + ) { + self.manager = manager + self.featureFlagger = featureFlagger + self.urlOpener = urlOpener + shouldShowMaliciousSiteProtectionSection = featureFlagger.isMaliciousSiteProtectionEnabled + isMaliciousSiteProtectionEnabled = manager.isEnabled + } + + func learnMoreAction() { + urlOpener.open(URL.maliciousSiteProtectionLearnMore) + } +} diff --git a/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift b/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift index 49f1321268..76d37dabf7 100644 --- a/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift +++ b/DuckDuckGo/MaliciousSiteProtection/UserPreferences/MaliciousSiteProtectionPreferencesManager.swift @@ -19,22 +19,44 @@ import Foundation import Combine +import Core -protocol MaliciousSiteProtectionPreferencesPublishing { +protocol MaliciousSiteProtectionPreferencesStorage: AnyObject { + var isEnabled: Bool { get set } +} + +final class MaliciousSiteProtectionPreferencesUserDefaultsStore: MaliciousSiteProtectionPreferencesStorage { + @UserDefaultsWrapper(key: .maliciousSiteProtectionEnabled, defaultValue: false) + var isEnabled: Bool +} + +protocol MaliciousSiteProtectionPreferencesReadable: AnyObject { var isEnabled: Bool { get } - var isEnabledPublisher: AnyPublisher { get } } -protocol MaliciousSiteProtectionPreferencesManaging { +protocol MaliciousSiteProtectionPreferencesWritable: AnyObject { var isEnabled: Bool { get set } } -final class MaliciousSiteProtectionPreferencesManager: MaliciousSiteProtectionPreferencesManaging, MaliciousSiteProtectionPreferencesPublishing { - @Published var isEnabled: Bool +protocol MaliciousSiteProtectionPreferencesPublishing: MaliciousSiteProtectionPreferencesReadable { + var isEnabledPublisher: AnyPublisher { get } +} + +typealias MaliciousSiteProtectionPreferencesManaging = MaliciousSiteProtectionPreferencesWritable & MaliciousSiteProtectionPreferencesPublishing + +final class MaliciousSiteProtectionPreferencesManager: MaliciousSiteProtectionPreferencesManaging { + @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/DuckDuckGo/SettingsGeneralView.swift b/DuckDuckGo/SettingsGeneralView.swift index 115c88a0fb..2e63dd124c 100644 --- a/DuckDuckGo/SettingsGeneralView.swift +++ b/DuckDuckGo/SettingsGeneralView.swift @@ -85,6 +85,9 @@ struct SettingsGeneralView: View { SettingsCellView(label: UserText.settingsAssociatedApps, accessory: .toggle(isOn: viewModel.universalLinksBinding)) } + + SettingsMaliciousProtectionView() + } .applySettingsListModifiers(title: UserText.general, displayMode: .inline, 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"; 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) + } +} 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..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 { @@ -125,3 +126,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