From d9b78c77e74998a686cbeb2ed44e468c44f5276e Mon Sep 17 00:00:00 2001 From: Sabrina Tardio Date: Thu, 13 Feb 2025 11:26:09 +0100 Subject: [PATCH 1/6] first draft implementation --- DuckDuckGo-iOS.xcodeproj/project.pbxproj | 9 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../NetworkProtectionDNSSettingsView.swift | 42 +++---- ...etworkProtectionDNSSettingsViewModel.swift | 43 +++++-- DuckDuckGo/NetworkProtectionUIElements.swift | 112 ++++++++++++++++++ .../NetworkProtectionVPNLocationView.swift | 54 +-------- DuckDuckGo/UserText.swift | 1 + DuckDuckGo/en.lproj/Localizable.strings | 5 +- 8 files changed, 177 insertions(+), 93 deletions(-) create mode 100644 DuckDuckGo/NetworkProtectionUIElements.swift diff --git a/DuckDuckGo-iOS.xcodeproj/project.pbxproj b/DuckDuckGo-iOS.xcodeproj/project.pbxproj index aaf4709850..7f7fac36c7 100644 --- a/DuckDuckGo-iOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-iOS.xcodeproj/project.pbxproj @@ -313,6 +313,7 @@ 569437362BE5160600C0881B /* SyncSettingsViewControllerErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569437352BE5160600C0881B /* SyncSettingsViewControllerErrorTests.swift */; }; 56A061442BEE086700F24B36 /* CapturingAdapterErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569437262BDD467400C0881B /* CapturingAdapterErrorHandler.swift */; }; 56A061452BEE086E00F24B36 /* CapturingSyncPausedStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569437372BE530D300C0881B /* CapturingSyncPausedStateManager.swift */; }; + 56CCBA2E2D5CF07D00EA0EB5 /* NetworkProtectionUIElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CCBA2D2D5CF07D00EA0EB5 /* NetworkProtectionUIElements.swift */; }; 56D060262C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D060252C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift */; }; 56D060282C380D83003BAEB5 /* OnboardingSuggestedSearchesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D060272C380D83003BAEB5 /* OnboardingSuggestedSearchesProvider.swift */; }; 56D0602D2C383FD2003BAEB5 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D0602C2C383FD2003BAEB5 /* OnboardingSuggestedSearchesProviderTests.swift */; }; @@ -1790,6 +1791,7 @@ 569437322BE4E3DD00C0881B /* SyncErrorHandlerSyncErrorsAlertsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorHandlerSyncErrorsAlertsTests.swift; sourceTree = ""; }; 569437352BE5160600C0881B /* SyncSettingsViewControllerErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSettingsViewControllerErrorTests.swift; sourceTree = ""; }; 569437372BE530D300C0881B /* CapturingSyncPausedStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturingSyncPausedStateManager.swift; sourceTree = ""; }; + 56CCBA2D2D5CF07D00EA0EB5 /* NetworkProtectionUIElements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUIElements.swift; sourceTree = ""; }; 56D060252C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingDialogs.swift; sourceTree = ""; }; 56D060272C380D83003BAEB5 /* OnboardingSuggestedSearchesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestedSearchesProvider.swift; sourceTree = ""; }; 56D0602C2C383FD2003BAEB5 /* OnboardingSuggestedSearchesProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestedSearchesProviderTests.swift; sourceTree = ""; }; @@ -6418,6 +6420,7 @@ 4B0F3F4F2B9BFF2100392892 /* NetworkProtectionFAQView.swift */, BD2F39EA2C19F955005B19E7 /* NetworkProtectionDNSSettingsView.swift */, BDF8D0012C1B87F4003E3B27 /* NetworkProtectionDNSSettingsViewModel.swift */, + 56CCBA2D2D5CF07D00EA0EB5 /* NetworkProtectionUIElements.swift */, ); name = VPNSettings; sourceTree = ""; @@ -8560,6 +8563,7 @@ C1935A122C88D1D8001AD72D /* AutofillHeaderViewFactory.swift in Sources */, 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, + 56CCBA2E2D5CF07D00EA0EB5 /* NetworkProtectionUIElements.swift in Sources */, 6F9FFE2D2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift in Sources */, CBF2597D2D41734700AC63E4 /* Terminating.swift in Sources */, D62EC3C22C248AF800FC9D04 /* DuckPlayerNavigationHandling.swift in Sources */, @@ -8653,7 +8657,6 @@ F194FAED1F14E2B3009B4DF8 /* UIFontExtension.swift in Sources */, 98F0FC2021FF18E700CE77AB /* AutoClearSettingsViewController.swift in Sources */, D67969112BC84CE700BA8B34 /* SubscriptionContainerViewModel.swift in Sources */, - 85DB12ED2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift in Sources */, 9F5179D82D54598800E40B40 /* DaxDialogsOnboardingMigrator.swift in Sources */, 98DA6ECA2181E41F00E65433 /* ThemeManager.swift in Sources */, F1D43AFC2B99C56000BAB743 /* RootDebugViewController+VanillaBrowser.swift in Sources */, @@ -12364,8 +12367,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit.git"; requirement = { - kind = exactVersion; - version = 236.1.0; + branch = "sabrina/risky-sites-protection"; + kind = branch; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 33243847b2..563b548a24 100644 --- a/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit.git", "state" : { - "revision" : "7356a39ecc21ec36cb1006a6cdacdae48fe1eaae", - "version" : "236.1.0" + "branch" : "sabrina/risky-sites-protection", + "revision" : "af0e405b22c2b750a99bdc208f1beb0efb764b0d" } }, { diff --git a/DuckDuckGo/NetworkProtectionDNSSettingsView.swift b/DuckDuckGo/NetworkProtectionDNSSettingsView.swift index 33698a2a83..852995d15a 100644 --- a/DuckDuckGo/NetworkProtectionDNSSettingsView.swift +++ b/DuckDuckGo/NetworkProtectionDNSSettingsView.swift @@ -21,7 +21,7 @@ import SwiftUI import NetworkProtection struct NetworkProtectionDNSSettingsView: View { - @StateObject var viewModel = NetworkProtectionDNSSettingsViewModel(settings: VPNSettings(defaults: .networkProtectionGroupDefaults)) + @StateObject var viewModel = NetworkProtectionDNSSettingsViewModel(settings: VPNSettings(defaults: .networkProtectionGroupDefaults), controller: AppDependencyProvider.shared.networkProtectionTunnelController) @Environment(\.dismiss) private var dismiss @FocusState private var isCustomDNSServerFocused: Bool @@ -29,14 +29,14 @@ struct NetworkProtectionDNSSettingsView: View { VStack { List { Section { - ChecklistItem(isSelected: !viewModel.isCustomDNSSelected) { + NetworkProtectionUIElements.ChecklistItem(isSelected: !viewModel.isCustomDNSSelected) { viewModel.toggleDNSSettings() } label: { Text(UserText.vpnSettingDNSServerOptionRecommended) .daxBodyRegular() .foregroundStyle(Color(designSystemColor: .textPrimary)) } - ChecklistItem(isSelected: viewModel.isCustomDNSSelected) { + NetworkProtectionUIElements.ChecklistItem(isSelected: viewModel.isCustomDNSSelected) { viewModel.toggleDNSSettings() } label: { Text(UserText.vpnSettingDNSServerOptionCustom) @@ -57,6 +57,8 @@ struct NetworkProtectionDNSSettingsView: View { if viewModel.isCustomDNSSelected { customDNSSection() + } else { + blockRiskyDomainsSection() } } } @@ -70,7 +72,7 @@ struct NetworkProtectionDNSSettingsView: View { } label: { Text(UserText.vpnSettingDNSServerApplyButtonTitle) } - .disabled(!viewModel.isApplyButtonEnabled) + .disabled(viewModel.isApplyButtonEnabled) } } } @@ -105,29 +107,15 @@ struct NetworkProtectionDNSSettingsView: View { isCustomDNSServerFocused = true } } -} - -private struct ChecklistItem: View where Content: View { - let isSelected: Bool - let action: () -> Void - @ViewBuilder let label: () -> Content - var body: some View { - Button( - action: action, - label: { - HStack(spacing: 12) { - label() - Spacer() - Image(systemName: "checkmark") - .tint(.init(designSystemColor: .accent)) - .if(!isSelected) { - $0.hidden() - } - } - } - ) - .tint(Color(designSystemColor: .textPrimary)) - .listRowInsets(EdgeInsets(top: 14, leading: 16, bottom: 14, trailing: 16)) + func blockRiskyDomainsSection() -> some View { + NetworkProtectionUIElements.ToggleSectionView( + text: "Block risky domains", + headerText: "Content Blocking and Filtering", + footerText: UserText.vpnContentBlockingFilteringFooter + ) { + Toggle("", isOn: $viewModel.isBlockRiskyDomainsOn) + } } + } diff --git a/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift b/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift index 429a9c0bf6..aac7e38cee 100644 --- a/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift +++ b/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift @@ -24,38 +24,57 @@ import Core final class NetworkProtectionDNSSettingsViewModel: ObservableObject { private let settings: VPNSettings + private let controller: TunnelController private var cancellables: Set = [] - @Published public var dnsSettings: NetworkProtectionDNSSettings = .default + @Published public var dnsSettings: NetworkProtectionDNSSettings @Published public var isCustomDNSSelected = false - + + @Published var isBlockRiskyDomainsOn: Bool { + didSet { + applyDNSSettings() + } + } + @Published public var customDNSServers = "" @Published public var isApplyButtonEnabled = false - init(settings: VPNSettings) { + init(settings: VPNSettings, controller: TunnelController) { self.settings = settings + self.controller = controller + + dnsSettings = settings.dnsSettings + isBlockRiskyDomainsOn = settings.isBlockRiskyDomainsOn + isCustomDNSSelected = settings.dnsSettings.usesCustomDNS + customDNSServers = settings.dnsSettings.dnsServersText + + subscribeToDNSSettingsChanges() + } + func subscribeToDNSSettingsChanges() { settings.dnsSettingsPublisher .receive(on: DispatchQueue.main) .assign(to: \.dnsSettings, onWeaklyHeld: self) .store(in: &cancellables) - - isCustomDNSSelected = settings.dnsSettings.usesCustomDNS - customDNSServers = settings.dnsSettings.dnsServersText } func toggleDNSSettings() { isCustomDNSSelected.toggle() } + func toggleIsBlockRiskyDomainsOn() { + isBlockRiskyDomainsOn.toggle() + } + func applyDNSSettings() { if isCustomDNSSelected { settings.dnsSettings = .custom([customDNSServers]) } else { - settings.dnsSettings = .default + settings.dnsSettings = .ddg(blockRiskyDomains: isBlockRiskyDomainsOn) } + reloadVPN() /// Updating `dnsSettings` does an IPv4 conversion before actually commiting the change, /// so we do a final check to see which outcome the user ends up with @@ -73,12 +92,20 @@ final class NetworkProtectionDNSSettingsViewModel: ObservableObject { isApplyButtonEnabled = true } } + + private func reloadVPN() { + Task { + // We need to allow some time for the setting to propagate + try await Task.sleep(interval: 0.1) + try await controller.command(.restartAdapter) + } + } } extension NetworkProtectionDNSSettings { fileprivate var dnsServersText: String { switch self { - case .default: return "" + case .ddg: return "" case .custom(let servers): return servers.joined(separator: ", ") } } diff --git a/DuckDuckGo/NetworkProtectionUIElements.swift b/DuckDuckGo/NetworkProtectionUIElements.swift new file mode 100644 index 0000000000..9ec3b299c7 --- /dev/null +++ b/DuckDuckGo/NetworkProtectionUIElements.swift @@ -0,0 +1,112 @@ +// +// NetworkProtectionUIElements.swift +// DuckDuckGo +// +// Copyright © 2025 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 SwiftUI + +struct NetworkProtectionUIElements { + + struct ChecklistItem: View where Content: View { + let isSelected: Bool + let action: () -> Void + @ViewBuilder let label: () -> Content + + var body: some View { + Button( + action: action, + label: { + HStack(spacing: 12) { + label() + Spacer() + Image(systemName: "checkmark") + .tint(.init(designSystemColor: .accent)) + .if(!isSelected) { + $0.hidden() + } + } + } + ) + .tint(Color(designSystemColor: .textPrimary)) + .listRowInsets(EdgeInsets(top: 14, leading: 16, bottom: 14, trailing: 16)) + } + } + + struct ToggleSectionView: View { + let text: String + let headerText: String + let footerText: String + let toggle: Content + + init(text: String, headerText: String, footerText: String, @ViewBuilder toggle: () -> Content) { + self.text = text + self.headerText = headerText + self.footerText = footerText + self.toggle = toggle() + } + + var body: some View { + Section { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(text) + .daxBodyRegular() + .foregroundColor(.init(designSystemColor: .textPrimary)) + .layoutPriority(1) + } + + toggle + .toggleStyle(SwitchToggleStyle(tint: .init(designSystemColor: .accent))) + } + } header: { + Text(headerText) + } footer: { + Text(LocalizedStringKey(footerText)) + .foregroundColor(.init(designSystemColor: .textSecondary)) + .accentColor(Color(designSystemColor: .accent)) + .daxFootnoteRegular() + .padding(.top, 6) + } + .listRowBackground(Color(designSystemColor: .surface)) + } + } + + struct MenuItem: View { + let isSelected: Bool + let title: String + let action: () -> Void + + var body: some View { + Button( + action: action, + label: { + HStack(spacing: 12) { + Text(title).daxBodyRegular() + Spacer() + Image(systemName: "checkmark") + .if(!isSelected) { + $0.hidden() + } + .tint(Color(designSystemColor: .textPrimary)) + } + } + ) + .tint(Color(designSystemColor: .textPrimary)) + } + } +} diff --git a/DuckDuckGo/NetworkProtectionVPNLocationView.swift b/DuckDuckGo/NetworkProtectionVPNLocationView.swift index eb49cbdff8..3727b9aef2 100644 --- a/DuckDuckGo/NetworkProtectionVPNLocationView.swift +++ b/DuckDuckGo/NetworkProtectionVPNLocationView.swift @@ -41,7 +41,7 @@ struct NetworkProtectionVPNLocationView: View { @ViewBuilder private func nearest(isSelected: Bool) -> some View { Section { - ChecklistItem( + NetworkProtectionUIElements.ChecklistItem( isSelected: isSelected, action: { Task { @@ -106,7 +106,7 @@ private struct CountryItem: View { } var body: some View { - ChecklistItem( + NetworkProtectionUIElements.ChecklistItem( isSelected: itemModel.isSelected, action: action, label: { @@ -125,7 +125,7 @@ private struct CountryItem: View { Spacer() Menu { ForEach(itemModel.cityPickerItems) { cityItem in - MenuItem(isSelected: cityItem.isSelected, title: cityItem.name) { + NetworkProtectionUIElements.MenuItem(isSelected: cityItem.isSelected, title: cityItem.name) { cityPickerAction(cityItem.id) } } @@ -140,51 +140,3 @@ private struct CountryItem: View { ) } } - -private struct ChecklistItem: View where Content: View { - let isSelected: Bool - let action: () -> Void - @ViewBuilder let label: () -> Content - - var body: some View { - Button( - action: action, - label: { - HStack(spacing: 12) { - Image(systemName: "checkmark") - .tint(.init(designSystemColor: .accent)) - .if(!isSelected) { - $0.hidden() - } - label() - } - } - ) - .tint(Color(designSystemColor: .textPrimary)) - .listRowInsets(EdgeInsets(top: 14, leading: 16, bottom: 14, trailing: 16)) - } -} - -private struct MenuItem: View { - let isSelected: Bool - let title: String - let action: () -> Void - - var body: some View { - Button( - action: action, - label: { - HStack(spacing: 12) { - Text(title).daxBodyRegular() - Spacer() - Image(systemName: "checkmark") - .if(!isSelected) { - $0.hidden() - } - .tint(Color(designSystemColor: .textPrimary)) - } - } - ) - .tint(Color(designSystemColor: .textPrimary)) - } -} diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 4ee19edc2c..2f221de9b1 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -800,6 +800,7 @@ public struct UserText { public static let vpnSettingDNSServerIPv4Title = NSLocalizedString("vpn.settings.dns.server.ipv4.title", value: "IPv4 Address", comment: "Title for the IPv4 Address setting") public static let vpnSettingDNSServerScreenTitle = NSLocalizedString("vpn.settings.dns.server.screen.title", value: "DNS Server", comment: "Title for the DNS Server setting screen") public static let vpnSettingDNSServerApplyButtonTitle = NSLocalizedString("vpn.settings.dns.server.apply.button.title", value: "Apply", comment: "Title for the Apply button on the DNS Server setting screen") + public static let vpnContentBlockingFilteringFooter = NSLocalizedString("settings.web.tracking.protection.explanation", value: "Block 150,000+ domains flagged for hosting malware, phishing attacks, and online scams with a DNS-level blocklist. [Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/)", comment: "Explanation in Settings how the vpn blocks risky sites (do not remove the link)") // MARK: Notifications diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index b93f926ffb..8375b92d8f 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -2461,8 +2461,9 @@ But if you *do* want a peek under the hood, you can find more information about /* The name of Settings category in Privacy Features related to configuration of the web tracking protection feature */ "settings.web.tracking.protection" = "Web Tracking Protection"; -/* Explanation in Settings how the web tracking protection feature works */ -"settings.web.tracking.protection.explanation" = "DuckDuckGo automatically blocks hidden trackers as you browse the web.\n[Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/)"; +/* Explanation in Settings how the vpn blocks risky sites (do not remove the link) + Explanation in Settings how the web tracking protection feature works */ +"settings.web.tracking.protection.explanation" = "Block 150,000+ domains flagged for hosting malware, phishing attacks, and online scams with a DNS-level blocklist. [Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/)"; /* Description on a report broken site page. */ "site.not.working.description" = "Select the option that best describes the problem you experienced."; From 2fac9a889bf2a6b8711418eac016a51259eb8ada Mon Sep 17 00:00:00 2001 From: Sabrina Tardio Date: Fri, 14 Feb 2025 10:36:46 +0100 Subject: [PATCH 2/6] add feature flag --- Core/FeatureFlag.swift | 6 ++++++ .../xcshareddata/swiftpm/Package.resolved | 2 +- DuckDuckGo/NetworkProtectionDNSSettingsView.swift | 4 +++- DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift | 9 ++++++++- DuckDuckGo/NetworkProtectionTunnelController.swift | 7 ++++++- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index d968072d0b..b642643075 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -78,6 +78,8 @@ public enum FeatureFlag: String { /// Feature flag to enable / disable phishing and malware protection /// https://app.asana.com/0/1206329551987282/1207149365636877/f case maliciousSiteProtection + + case networkProtectionRiskyDomainsProtection } extension FeatureFlag: FeatureFlagDescribing { @@ -100,6 +102,8 @@ extension FeatureFlag: FeatureFlagDescribing { return true case .testExperiment: return true + case .networkProtectionRiskyDomainsProtection: + return true default: return false } @@ -185,6 +189,8 @@ extension FeatureFlag: FeatureFlagDescribing { return .remoteReleasable(.subfeature(ExperimentTestSubfeatures.experimentTestAA)) case .maliciousSiteProtection: return .remoteReleasable(.subfeature(MaliciousSiteProtectionSubfeature.onByDefault)) + case .networkProtectionRiskyDomainsProtection: + return .remoteReleasable(.subfeature(NetworkProtectionSubfeature.riskyDomainsProtection)) } } } diff --git a/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 563b548a24..b383cab378 100644 --- a/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/DuckDuckGo/BrowserServicesKit.git", "state" : { "branch" : "sabrina/risky-sites-protection", - "revision" : "af0e405b22c2b750a99bdc208f1beb0efb764b0d" + "revision" : "9a6d1f4f0220da9639e72cf81bd40554341f106c" } }, { diff --git a/DuckDuckGo/NetworkProtectionDNSSettingsView.swift b/DuckDuckGo/NetworkProtectionDNSSettingsView.swift index 852995d15a..4f6a3047a2 100644 --- a/DuckDuckGo/NetworkProtectionDNSSettingsView.swift +++ b/DuckDuckGo/NetworkProtectionDNSSettingsView.swift @@ -58,7 +58,9 @@ struct NetworkProtectionDNSSettingsView: View { if viewModel.isCustomDNSSelected { customDNSSection() } else { - blockRiskyDomainsSection() + if viewModel.isRiskySitesProtectionFeatureEnabled { + blockRiskyDomainsSection() + } } } } diff --git a/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift b/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift index aac7e38cee..e75292aa75 100644 --- a/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift +++ b/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift @@ -21,10 +21,12 @@ import Foundation import Combine import NetworkProtection import Core +import BrowserServicesKit final class NetworkProtectionDNSSettingsViewModel: ObservableObject { private let settings: VPNSettings private let controller: TunnelController + private let featureFlagger: FeatureFlagger private var cancellables: Set = [] @Published public var dnsSettings: NetworkProtectionDNSSettings @@ -41,9 +43,14 @@ final class NetworkProtectionDNSSettingsViewModel: ObservableObject { @Published public var isApplyButtonEnabled = false - init(settings: VPNSettings, controller: TunnelController) { + var isRiskySitesProtectionFeatureEnabled: Bool { + featureFlagger.isFeatureOn(.networkProtectionRiskyDomainsProtection) + } + + init(settings: VPNSettings, controller: TunnelController, featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger) { self.settings = settings self.controller = controller + self.featureFlagger = featureFlagger dnsSettings = settings.dnsSettings isBlockRiskyDomainsOn = settings.isBlockRiskyDomainsOn diff --git a/DuckDuckGo/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift index ed22d7cd91..b788a56757 100644 --- a/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -292,7 +292,12 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } options[NetworkProtectionOptionKey.selectedEnvironment] = AppDependencyProvider.shared.vpnSettings .selectedEnvironment.rawValue as NSString - if let data = try? JSONEncoder().encode(AppDependencyProvider.shared.vpnSettings.dnsSettings) { + + var dnsSettings = settings.dnsSettings + if dnsSettings == .ddg(blockRiskyDomains: true) && !featureFlagger.isFeatureOn(.networkProtectionRiskyDomainsProtection) { + dnsSettings = .ddg(blockRiskyDomains: false) + } + if let data = try? JSONEncoder().encode(dnsSettings) { options[NetworkProtectionOptionKey.dnsSettings] = NSData(data: data) } From 6afb1eb086dd2891329cc0c84ceaf71b82315ee2 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio Date: Fri, 14 Feb 2025 11:03:02 +0100 Subject: [PATCH 3/6] fix custom field and show last used IP --- DuckDuckGo/NetworkProtectionDNSSettingsView.swift | 2 +- DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/NetworkProtectionDNSSettingsView.swift b/DuckDuckGo/NetworkProtectionDNSSettingsView.swift index 4f6a3047a2..e35db4aaf3 100644 --- a/DuckDuckGo/NetworkProtectionDNSSettingsView.swift +++ b/DuckDuckGo/NetworkProtectionDNSSettingsView.swift @@ -74,7 +74,7 @@ struct NetworkProtectionDNSSettingsView: View { } label: { Text(UserText.vpnSettingDNSServerApplyButtonTitle) } - .disabled(viewModel.isApplyButtonEnabled) + .disabled(!viewModel.isApplyButtonEnabled) } } } diff --git a/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift b/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift index e75292aa75..e0159675a5 100644 --- a/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift +++ b/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift @@ -55,7 +55,7 @@ final class NetworkProtectionDNSSettingsViewModel: ObservableObject { dnsSettings = settings.dnsSettings isBlockRiskyDomainsOn = settings.isBlockRiskyDomainsOn isCustomDNSSelected = settings.dnsSettings.usesCustomDNS - customDNSServers = settings.dnsSettings.dnsServersText + customDNSServers = settings.customDnsServers.joined(separator: ", ") subscribeToDNSSettingsChanges() } From b82c0b20d50bef15c94c9ba8b1d1f608f4f2ff90 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio Date: Fri, 14 Feb 2025 14:13:20 +0100 Subject: [PATCH 4/6] add test file --- DuckDuckGo-iOS.xcodeproj/project.pbxproj | 4 + ...kProtectionDNSSettingsViewModelTests.swift | 76 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 DuckDuckGoTests/NetworkProtectionDNSSettingsViewModelTests.swift diff --git a/DuckDuckGo-iOS.xcodeproj/project.pbxproj b/DuckDuckGo-iOS.xcodeproj/project.pbxproj index 7f7fac36c7..130fa1b49f 100644 --- a/DuckDuckGo-iOS.xcodeproj/project.pbxproj +++ b/DuckDuckGo-iOS.xcodeproj/project.pbxproj @@ -317,6 +317,7 @@ 56D060262C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D060252C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift */; }; 56D060282C380D83003BAEB5 /* OnboardingSuggestedSearchesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D060272C380D83003BAEB5 /* OnboardingSuggestedSearchesProvider.swift */; }; 56D0602D2C383FD2003BAEB5 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D0602C2C383FD2003BAEB5 /* OnboardingSuggestedSearchesProviderTests.swift */; }; + 56D2D65C2D5F748200C59354 /* NetworkProtectionDNSSettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D2D65B2D5F748200C59354 /* NetworkProtectionDNSSettingsViewModelTests.swift */; }; 56D7792C2CFF476800B619EF /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D664C7DC2B28A02800CBFA76 /* StoreKit.framework */; }; 56D7793A2CFFC7E800B619EF /* PixelExperimentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 56D779392CFFC7E800B619EF /* PixelExperimentKit */; }; 56D7793C2CFFC7E800B619EF /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 56D7793B2CFFC7E800B619EF /* PixelKit */; }; @@ -1795,6 +1796,7 @@ 56D060252C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingDialogs.swift; sourceTree = ""; }; 56D060272C380D83003BAEB5 /* OnboardingSuggestedSearchesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestedSearchesProvider.swift; sourceTree = ""; }; 56D0602C2C383FD2003BAEB5 /* OnboardingSuggestedSearchesProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestedSearchesProviderTests.swift; sourceTree = ""; }; + 56D2D65B2D5F748200C59354 /* NetworkProtectionDNSSettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDNSSettingsViewModelTests.swift; sourceTree = ""; }; 56D855692BEA9169009F9698 /* CurrentDateProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentDateProviding.swift; sourceTree = ""; }; 56D8556B2BEA91C4009F9698 /* SyncAlertsPresenting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncAlertsPresenting.swift; sourceTree = ""; }; 653561012D4D2C680064F258 /* Logger+DuckPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+DuckPlayer.swift"; sourceTree = ""; }; @@ -5316,6 +5318,7 @@ children = ( 981FED682201FE69008488D7 /* AutoClearSettingsScreenTests.swift */, 8598F6792405EB8600FBC70C /* KeyboardSettingsTests.swift */, + 56D2D65B2D5F748200C59354 /* NetworkProtectionDNSSettingsViewModelTests.swift */, ); name = Settings; sourceTree = ""; @@ -9074,6 +9077,7 @@ C14882EA27F20DD000D59F0C /* MockBookmarksCoreDataStorage.swift in Sources */, 6594CCEA2D4A3E1200188B1A /* DuckPlayerSettingsTests.swift in Sources */, 1E05D1DB29C47B3300BF9A1F /* DailyPixelTests.swift in Sources */, + 56D2D65C2D5F748200C59354 /* NetworkProtectionDNSSettingsViewModelTests.swift in Sources */, 564DE4552C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift in Sources */, 6F5AA3EF2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift in Sources */, 981FED7422046017008488D7 /* AutoClearTests.swift in Sources */, diff --git a/DuckDuckGoTests/NetworkProtectionDNSSettingsViewModelTests.swift b/DuckDuckGoTests/NetworkProtectionDNSSettingsViewModelTests.swift new file mode 100644 index 0000000000..43301cc7cd --- /dev/null +++ b/DuckDuckGoTests/NetworkProtectionDNSSettingsViewModelTests.swift @@ -0,0 +1,76 @@ +//// +//// NetworkProtectionDNSSettingsViewModelTests.swift +//// DuckDuckGo +//// +//// Copyright © 2025 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 XCTest +//import NetworkProtection +//@testable import DuckDuckGo +// +//final class NetworkProtectionDNSSettingsViewModelTests: XCTestCase { +// +// var model: NetworkProtectionDNSSettingsViewModel! +// let userDefaults = UserDefaults(suiteName: "TestDefaults")! +// var vpnSettings: VPNSettings! +// +// override func setUpWithError() throws { +// vpnSettings = VPNSettings(defaults: userDefaults) +// model = NetworkProtectionDNSSettingsViewModel(settings: vpnSettings, controller: MockTunnelController(), featureFlagger: MockFeatureFlagger()) +// } +// +// override func tearDownWithError() throws { +// userDefaults.removePersistentDomain(forName: "TestDefaults") +// vpnSettings = nil +// model = nil +// } +// +// func testInitialState() { +// XCTAssertEqual(model.dnsSettings, vpnSettings.dnsSettings) +// XCTAssertFalse(model.isCustomDNSSelected) +// XCTAssertEqual(model.customDNSServers, vpnSettings.customDnsServers.joined(separator: ", ")) +// XCTAssertTrue(model.isBlockRiskyDomainsOn) +// } +// +// func test_WhenUpdateDNSSettingsToCustomThenPropagatesToVpnSettings() { +// // WHEN +// model.isCustomDNSSelected = true +// model.customDNSServers = "1.1.1.1, 8.8.8.8" +// +// // THEN +// switch vpnSettings.dnsSettings { +// case .custom(let servers): +// XCTAssertEqual(servers, ["1.1.1.1", "8.8.8.8"], "Custom DNS servers should be updated correctly.") +// default: +// XCTFail("Expected dnsSettings to be .custom, but got \(vpnSettings.dnsSettings)") +// } +// } +// +// +//} +// +//final class MockTunnelController: TunnelController { +// func start() async { +// } +// +// func stop() async { +// } +// +// func command(_ command: NetworkProtection.VPNCommand) async throws { +// } +// +// var isConnected: Bool = false +//} From 2274be28634977ec5a21847741084d4b24ed54e8 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio Date: Fri, 14 Feb 2025 15:06:16 +0100 Subject: [PATCH 5/6] add tests --- .../xcshareddata/swiftpm/Package.resolved | 14 +- ...etworkProtectionDNSSettingsViewModel.swift | 4 +- ...kProtectionDNSSettingsViewModelTests.swift | 313 ++++++++++++++---- 3 files changed, 252 insertions(+), 79 deletions(-) diff --git a/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b383cab378..83f7be8847 100644 --- a/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/DuckDuckGo/BrowserServicesKit.git", "state" : { "branch" : "sabrina/risky-sites-protection", - "revision" : "9a6d1f4f0220da9639e72cf81bd40554341f106c" + "revision" : "c933d36a84b488150eedce6425ed8c3f5c38ba1f" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "1876d68142cf4f9abfaaee235a015d287eb71226", - "version" : "7.17.0" + "revision" : "874b27ad51d1784c934760c85493f78e609c4409", + "version" : "7.18.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "676d42679296a175169e433f2332e55644151edd", - "version" : "16.2.0" + "revision" : "3a9606fd26e9a54bf369cc241e6fa3b2571eb13a", + "version" : "16.2.1" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "099f7ed5faac946e4d80746703aaaf87fdfbee09", - "version" : "8.3.0" + "revision" : "c05d7ce7debe45593bfd9759d6bcae0cd740c52f", + "version" : "8.4.0" } }, { diff --git a/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift b/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift index e0159675a5..83e8691153 100644 --- a/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift +++ b/DuckDuckGo/NetworkProtectionDNSSettingsViewModel.swift @@ -31,7 +31,7 @@ final class NetworkProtectionDNSSettingsViewModel: ObservableObject { @Published public var dnsSettings: NetworkProtectionDNSSettings - @Published public var isCustomDNSSelected = false + @Published public var isCustomDNSSelected: Bool @Published var isBlockRiskyDomainsOn: Bool { didSet { @@ -39,7 +39,7 @@ final class NetworkProtectionDNSSettingsViewModel: ObservableObject { } } - @Published public var customDNSServers = "" + @Published public var customDNSServers: String @Published public var isApplyButtonEnabled = false diff --git a/DuckDuckGoTests/NetworkProtectionDNSSettingsViewModelTests.swift b/DuckDuckGoTests/NetworkProtectionDNSSettingsViewModelTests.swift index 43301cc7cd..c7790999c6 100644 --- a/DuckDuckGoTests/NetworkProtectionDNSSettingsViewModelTests.swift +++ b/DuckDuckGoTests/NetworkProtectionDNSSettingsViewModelTests.swift @@ -1,76 +1,249 @@ -//// -//// NetworkProtectionDNSSettingsViewModelTests.swift -//// DuckDuckGo -//// -//// Copyright © 2025 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 XCTest -//import NetworkProtection -//@testable import DuckDuckGo +// NetworkProtectionDNSSettingsViewModelTests.swift +// DuckDuckGo // -//final class NetworkProtectionDNSSettingsViewModelTests: XCTestCase { +// Copyright © 2025 DuckDuckGo. All rights reserved. // -// var model: NetworkProtectionDNSSettingsViewModel! -// let userDefaults = UserDefaults(suiteName: "TestDefaults")! -// var vpnSettings: VPNSettings! +// 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 // -// override func setUpWithError() throws { -// vpnSettings = VPNSettings(defaults: userDefaults) -// model = NetworkProtectionDNSSettingsViewModel(settings: vpnSettings, controller: MockTunnelController(), featureFlagger: MockFeatureFlagger()) -// } +// http://www.apache.org/licenses/LICENSE-2.0 // -// override func tearDownWithError() throws { -// userDefaults.removePersistentDomain(forName: "TestDefaults") -// vpnSettings = nil -// model = nil -// } +// 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. // -// func testInitialState() { -// XCTAssertEqual(model.dnsSettings, vpnSettings.dnsSettings) -// XCTAssertFalse(model.isCustomDNSSelected) -// XCTAssertEqual(model.customDNSServers, vpnSettings.customDnsServers.joined(separator: ", ")) -// XCTAssertTrue(model.isBlockRiskyDomainsOn) -// } -// -// func test_WhenUpdateDNSSettingsToCustomThenPropagatesToVpnSettings() { -// // WHEN -// model.isCustomDNSSelected = true -// model.customDNSServers = "1.1.1.1, 8.8.8.8" -// -// // THEN -// switch vpnSettings.dnsSettings { -// case .custom(let servers): -// XCTAssertEqual(servers, ["1.1.1.1", "8.8.8.8"], "Custom DNS servers should be updated correctly.") -// default: -// XCTFail("Expected dnsSettings to be .custom, but got \(vpnSettings.dnsSettings)") -// } -// } -// -// -//} -// -//final class MockTunnelController: TunnelController { -// func start() async { -// } -// -// func stop() async { -// } -// -// func command(_ command: NetworkProtection.VPNCommand) async throws { -// } -// -// var isConnected: Bool = false -//} + +import XCTest +import NetworkProtection +@testable import DuckDuckGo + +final class NetworkProtectionDNSSettingsViewModelTests: XCTestCase { + + var model: NetworkProtectionDNSSettingsViewModel! + let userDefaults = UserDefaults(suiteName: "TestDefaults")! + var vpnSettings: VPNSettings! + + override func setUpWithError() throws { + vpnSettings = VPNSettings(defaults: userDefaults) + model = NetworkProtectionDNSSettingsViewModel(settings: vpnSettings, controller: MockTunnelController(), featureFlagger: MockFeatureFlagger()) + } + + override func tearDownWithError() throws { + userDefaults.removePersistentDomain(forName: "TestDefaults") + vpnSettings = nil + model = nil + } + + func testInitialState() { + XCTAssertEqual(model.dnsSettings, vpnSettings.dnsSettings) + XCTAssertFalse(model.isCustomDNSSelected) + XCTAssertEqual(model.customDNSServers, vpnSettings.customDnsServers.joined(separator: ", ")) + XCTAssertTrue(model.isBlockRiskyDomainsOn) + } + + func test_WhenUpdateDNSSettingsToCustomThenPropagatesToVpnSettings() { + // GIVEN + model.customDNSServers = "1.1.1.1" + model.isCustomDNSSelected = true + + // WHEN + model.applyDNSSettings() + + // THEN + switch vpnSettings.dnsSettings { + case .custom(let servers): + XCTAssertEqual(servers, ["1.1.1.1"], "Custom DNS servers should be updated correctly.") + default: + XCTFail("Expected dnsSettings to be .custom, but got \(vpnSettings.dnsSettings)") + } + } + + func test_WhenUpdateDNSSettingsToDefaultWithThenPropagatesToVpnSettings() { + // GIVEN + model.isCustomDNSSelected = false + model.isBlockRiskyDomainsOn = true + + // WHEN + model.applyDNSSettings() + + // THEN + switch vpnSettings.dnsSettings { + case .ddg(let blockRiskyDomains): + XCTAssertTrue(blockRiskyDomains, "Expected blockRiskyDomains to be false.") + default: + XCTFail("Expected dnsSettings to be .ddg, but got \(vpnSettings.dnsSettings)") + } + } + + func test_WhenUpdateDNSSettingsToDefaultWithBlockOffThenPropagatesToVpnSettings() { + // GIVEN + model.isCustomDNSSelected = false + model.isBlockRiskyDomainsOn = false + + // WHEN + model.applyDNSSettings() + + // THEN + switch vpnSettings.dnsSettings { + case .ddg(let blockRiskyDomains): + XCTAssertFalse(blockRiskyDomains, "Expected blockRiskyDomains to be false.") + default: + XCTFail("Expected dnsSettings to be .ddg, but got \(vpnSettings.dnsSettings)") + } + } + + func test_WhenMovingFromDefaultToCustomAndBackToDefaultThenBlockSettingRetainedToFalse() { + // GIVEN + model.isCustomDNSSelected = false + model.isBlockRiskyDomainsOn = false + model.applyDNSSettings() + model.isCustomDNSSelected = true + model.customDNSServers = "1.1.1.1" + model.applyDNSSettings() + + // WHEN + model.isCustomDNSSelected = false + model.applyDNSSettings() + + // THEN + switch vpnSettings.dnsSettings { + case .ddg(let blockRiskyDomains): + XCTAssertFalse(blockRiskyDomains, "Expected blockRiskyDomains to be false.") + default: + XCTFail("Expected dnsSettings to be .ddg, but got \(vpnSettings.dnsSettings)") + } + } + + func test_WhenMovingFromDefaultToCustomAndBackToDefaultThenBlockSettingRetainedToTrue() { + // GIVEN + model.isCustomDNSSelected = false + model.isBlockRiskyDomainsOn = true + model.applyDNSSettings() + model.isCustomDNSSelected = true + model.customDNSServers = "1.1.1.1" + model.applyDNSSettings() + + // WHEN + model.isCustomDNSSelected = false + model.applyDNSSettings() + + // THEN + switch vpnSettings.dnsSettings { + case .ddg(let blockRiskyDomains): + XCTAssertTrue(blockRiskyDomains, "Expected blockRiskyDomains to be true.") + default: + XCTFail("Expected dnsSettings to be .ddg, but got \(vpnSettings.dnsSettings)") + } + } + + func test_WhenMovingFromCustomToDefaultAndBackToCustomThenPreviouslySelectedServerRetained() { + // GIVEN + model.isCustomDNSSelected = true + model.customDNSServers = "1.1.1.1" + model.applyDNSSettings() + model.isCustomDNSSelected = false + model.applyDNSSettings() + + // WHEN + model.isCustomDNSSelected = true + model.applyDNSSettings() + + // THEN + switch vpnSettings.dnsSettings { + case .custom(let servers): + XCTAssertEqual(servers, ["1.1.1.1"], "Custom DNS servers should be updated correctly.") + default: + XCTFail("Expected dnsSettings to be .custom, but got \(vpnSettings.dnsSettings)") + } + } + + func testWhenUpdateDNSSettingsToCustomAndNoServerProvidedPreviousDnsSettingApplies() { + // GIVEN + model.isCustomDNSSelected = false + model.applyDNSSettings() + let previousDNS = vpnSettings.dnsSettings + + // WHEN + model.customDNSServers = "" + model.isCustomDNSSelected = true + model.applyDNSSettings() + + // THEN + XCTAssertEqual(vpnSettings.dnsSettings, previousDNS, "DNS settings should remain unchanged when no custom DNS is provided.") + } + + func testToggleDNSSettings() { + // GIVEN + let initial = model.isCustomDNSSelected + + // WHEN + model.toggleDNSSettings() + + // THEN + XCTAssertEqual(model.isCustomDNSSelected, !initial, "toggleDNSSettings should invert the value.") + } + + func testToggleIsBlockRiskyDomainsOn() { + // GIVEN + let initial = model.isBlockRiskyDomainsOn + + // WHEN + model.toggleIsBlockRiskyDomainsOn() + + // THEN + XCTAssertEqual(model.isBlockRiskyDomainsOn, !initial, "toggleIsBlockRiskyDomainsOn should invert the value.") + } + + func testUpdateApplyButtonStateWhenValid() { + // GIVEN + model.isCustomDNSSelected = true + model.customDNSServers = "1.1.1.1" + + // WHEN + model.updateApplyButtonState() + + // THEN + XCTAssertTrue(model.isApplyButtonEnabled, "Apply button should be enabled for valid custom DNS.") + } + + func testUpdateApplyButtonStateWhenInvalid() { + // GIVEN + model.isCustomDNSSelected = true + model.customDNSServers = "invalid" + + // WHEN + model.updateApplyButtonState() + + // THEN + XCTAssertFalse(model.isApplyButtonEnabled, "Apply button should be disabled for invalid custom DNS.") + } + + func testUpdateApplyButtonStateWhenDefault() { + // GIVEN + model.isCustomDNSSelected = false + + // WHEN + model.updateApplyButtonState() + + // THEN + XCTAssertTrue(model.isApplyButtonEnabled, "Apply button should be enabled in default mode.") + } + +} + +private final class MockTunnelController: TunnelController { + func start() async { + } + + func stop() async { + } + + func command(_ command: NetworkProtection.VPNCommand) async throws { + } + + var isConnected: Bool = false +} From 42cad50a6fed969a7c827b7850bbab7a60fbca99 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio Date: Fri, 14 Feb 2025 15:26:11 +0100 Subject: [PATCH 6/6] update url --- DuckDuckGo/UserText.swift | 2 +- DuckDuckGo/en.lproj/Localizable.strings | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 2f221de9b1..ddc0842421 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -800,7 +800,7 @@ public struct UserText { public static let vpnSettingDNSServerIPv4Title = NSLocalizedString("vpn.settings.dns.server.ipv4.title", value: "IPv4 Address", comment: "Title for the IPv4 Address setting") public static let vpnSettingDNSServerScreenTitle = NSLocalizedString("vpn.settings.dns.server.screen.title", value: "DNS Server", comment: "Title for the DNS Server setting screen") public static let vpnSettingDNSServerApplyButtonTitle = NSLocalizedString("vpn.settings.dns.server.apply.button.title", value: "Apply", comment: "Title for the Apply button on the DNS Server setting screen") - public static let vpnContentBlockingFilteringFooter = NSLocalizedString("settings.web.tracking.protection.explanation", value: "Block 150,000+ domains flagged for hosting malware, phishing attacks, and online scams with a DNS-level blocklist. [Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/)", comment: "Explanation in Settings how the vpn blocks risky sites (do not remove the link)") + public static let vpnContentBlockingFilteringFooter = NSLocalizedString("settings.web.tracking.protection.explanation", value: "Block 150,000+ domains flagged for hosting malware, phishing attacks, and online scams with a DNS-level blocklist. [Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/dns-blocklists/)", comment: "Explanation in Settings how the vpn blocks risky sites (do not remove the link)") // MARK: Notifications diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 8375b92d8f..ee204ca6a4 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -2463,7 +2463,7 @@ But if you *do* want a peek under the hood, you can find more information about /* Explanation in Settings how the vpn blocks risky sites (do not remove the link) Explanation in Settings how the web tracking protection feature works */ -"settings.web.tracking.protection.explanation" = "Block 150,000+ domains flagged for hosting malware, phishing attacks, and online scams with a DNS-level blocklist. [Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/)"; +"settings.web.tracking.protection.explanation" = "Block 150,000+ domains flagged for hosting malware, phishing attacks, and online scams with a DNS-level blocklist. [Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/dns-blocklists/)"; /* Description on a report broken site page. */ "site.not.working.description" = "Select the option that best describes the problem you experienced.";