diff --git a/Core/NetworkProtectionNotificationIdentifier.swift b/Core/NetworkProtectionNotificationIdentifier.swift new file mode 100644 index 0000000000..66a29dcebd --- /dev/null +++ b/Core/NetworkProtectionNotificationIdentifier.swift @@ -0,0 +1,28 @@ +// +// NetworkProtectionNotificationIdentifier.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public enum NetworkProtectionNotificationIdentifier: String { + case reconnecting = "network-protection.notification.reconnecting" + case reconnected = "network-protection.notification.reconnected" + case connectionFailure = "network-protection.notification.connection-failure" + case superseded = "network-protection.notification.superseded" + case test = "network-protection.notification.test" +} diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7ac2895c20..bba49be20b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -746,6 +746,7 @@ EE0153ED2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153EC2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift */; }; EE0153EF2A70021E002A8B26 /* NetworkProtectionInviteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153EE2A70021E002A8B26 /* NetworkProtectionInviteView.swift */; }; EE276BEA2A77F823009167B6 /* NetworkProtectionRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE276BE92A77F823009167B6 /* NetworkProtectionRootViewController.swift */; }; + EE3766DE2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */; }; EE3B226B29DE0F110082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; }; EE3B226C29DE0FD30082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; }; EE41BD192A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE41BD182A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift */; }; @@ -758,11 +759,13 @@ EE50053029C3BA0800AE0773 /* InternalUserStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE50052F29C3BA0800AE0773 /* InternalUserStore.swift */; }; EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE72CA842A862D000043B5B3 /* NetworkProtectionDebugViewController.swift */; }; EE7917912A83DE93008DFF28 /* CombineTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7917902A83DE93008DFF28 /* CombineTestUtilities.swift */; }; + EE7A92872AC6DE4700832A36 /* NetworkProtectionNotificationIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7A92862AC6DE4700832A36 /* NetworkProtectionNotificationIdentifier.swift */; }; EE8594992A44791C008A6D06 /* NetworkProtectionTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8594982A44791C008A6D06 /* NetworkProtectionTunnelController.swift */; }; EE8E568A2A56BCE400F11DCA /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE8E56892A56BCE400F11DCA /* NetworkProtection */; }; EEEB80A32A421CE600386378 /* NetworkProtectionPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEB80A22A421CE600386378 /* NetworkProtectionPacketTunnelProvider.swift */; }; EEF0F8CC2ABC832300630031 /* NetworkProtectionDebugFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF0F8CB2ABC832200630031 /* NetworkProtectionDebugFeatures.swift */; }; EEFAB4672A73C230008A38E4 /* NetworkProtectionTestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = EEFAB4662A73C230008A38E4 /* NetworkProtectionTestUtils */; }; + EEFC6A602AC0F2F80065027D /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEFC6A5F2AC0F2F80065027D /* UserText.swift */; }; EEFD562F2A65B6CA00DAEC48 /* NetworkProtectionInviteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEFD562E2A65B6CA00DAEC48 /* NetworkProtectionInviteViewModel.swift */; }; EEFE9C732A603CE9005B0A26 /* NetworkProtectionStatusViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEFE9C722A603CE9005B0A26 /* NetworkProtectionStatusViewModelTests.swift */; }; F103073B1E7C91330059FEC7 /* BookmarksDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F103073A1E7C91330059FEC7 /* BookmarksDataSource.swift */; }; @@ -2332,6 +2335,7 @@ EE0153EC2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRootView.swift; sourceTree = ""; }; EE0153EE2A70021E002A8B26 /* NetworkProtectionInviteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteView.swift; sourceTree = ""; }; EE276BE92A77F823009167B6 /* NetworkProtectionRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRootViewController.swift; sourceTree = ""; }; + EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationPresenter.swift; sourceTree = ""; }; EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInternalUserStoring.swift; sourceTree = ""; }; EE3B98EA2A9634CC002F63A0 /* DuckDuckGoAlpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoAlpha.entitlements; sourceTree = ""; }; EE3B98EB2A963515002F63A0 /* WidgetsExtensionAlpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetsExtensionAlpha.entitlements; sourceTree = ""; }; @@ -2346,10 +2350,12 @@ EE50052F29C3BA0800AE0773 /* InternalUserStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalUserStore.swift; sourceTree = ""; }; EE72CA842A862D000043B5B3 /* NetworkProtectionDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugViewController.swift; sourceTree = ""; }; EE7917902A83DE93008DFF28 /* CombineTestUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTestUtilities.swift; sourceTree = ""; }; + EE7A92862AC6DE4700832A36 /* NetworkProtectionNotificationIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNotificationIdentifier.swift; sourceTree = ""; }; EE8594982A44791C008A6D06 /* NetworkProtectionTunnelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTunnelController.swift; sourceTree = ""; }; EEB8FDB92A990AEE00EBEDCF /* Configuration-Alpha.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Configuration-Alpha.xcconfig"; path = "Configuration/Configuration-Alpha.xcconfig"; sourceTree = ""; }; EEEB80A22A421CE600386378 /* NetworkProtectionPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionPacketTunnelProvider.swift; sourceTree = ""; }; EEF0F8CB2ABC832200630031 /* NetworkProtectionDebugFeatures.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugFeatures.swift; sourceTree = ""; }; + EEFC6A5F2AC0F2F80065027D /* UserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserText.swift; sourceTree = ""; }; EEFD562E2A65B6CA00DAEC48 /* NetworkProtectionInviteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteViewModel.swift; sourceTree = ""; }; EEFE9C722A603CE9005B0A26 /* NetworkProtectionStatusViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionStatusViewModelTests.swift; sourceTree = ""; }; F103073A1E7C91330059FEC7 /* BookmarksDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksDataSource.swift; sourceTree = ""; }; @@ -2604,13 +2610,14 @@ 02025665298818B200E694E7 /* PacketTunnelProvider */ = { isa = PBXGroup; children = ( + EE3766DC2AC5940A00AAB575 /* NetworkProtection */, EE3B98EC2A963538002F63A0 /* PacketTunnelProviderAlpha.entitlements */, 02025670298818CB00E694E7 /* ProxyServer */, 02025666298818B200E694E7 /* AppTrackingProtectionPacketTunnelProvider.swift */, 02025B1429884EA500E694E7 /* DDGObserverFactory.swift */, 02025668298818B200E694E7 /* Info.plist */, 02025669298818B200E694E7 /* PacketTunnelProvider.entitlements */, - EEEB80A22A421CE600386378 /* NetworkProtectionPacketTunnelProvider.swift */, + EEFC6A5F2AC0F2F80065027D /* UserText.swift */, ); path = PacketTunnelProvider; sourceTree = ""; @@ -4344,6 +4351,15 @@ name = Root; sourceTree = ""; }; + EE3766DC2AC5940A00AAB575 /* NetworkProtection */ = { + isa = PBXGroup; + children = ( + EEEB80A22A421CE600386378 /* NetworkProtectionPacketTunnelProvider.swift */, + EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */, + ); + path = NetworkProtection; + sourceTree = ""; + }; EE3B226929DE0EE10082298A /* FeatureFlags */ = { isa = PBXGroup; children = ( @@ -4380,6 +4396,14 @@ name = NetworkProtection; sourceTree = ""; }; + EE7A92852AC6DE2500832A36 /* NetworkProtection */ = { + isa = PBXGroup; + children = ( + EE7A92862AC6DE4700832A36 /* NetworkProtectionNotificationIdentifier.swift */, + ); + name = NetworkProtection; + sourceTree = ""; + }; EECD94B22A28B8580085C66E /* NetworkProtection */ = { isa = PBXGroup; children = ( @@ -4655,6 +4679,7 @@ F143C2E51E4A4CD400CFDE3A /* Core */ = { isa = PBXGroup; children = ( + EE7A92852AC6DE2500832A36 /* NetworkProtection */, 4B470ED4299C484B0086EBDC /* AppTrackingProtection */, F1CE42A71ECA0A520074A8DF /* Bookmarks */, 837774491F8E1ECE00E17A29 /* ContentBlocker */, @@ -5962,6 +5987,7 @@ 4BEF656D2989C2FC00B650CB /* EventType.swift in Sources */, 02025AAC2988229800E694E7 /* GCDHTTPProxyServer.swift in Sources */, 02025AAD2988229800E694E7 /* NWUDPSocket.swift in Sources */, + EE3766DE2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift in Sources */, 02025AAE2988229800E694E7 /* RawTCPSocketProtocol.swift in Sources */, 02025AAF2988229800E694E7 /* NWTCPSocket.swift in Sources */, 02025AB12988229800E694E7 /* RawSocketFactory.swift in Sources */, @@ -6005,6 +6031,7 @@ 02025AEB2988229800E694E7 /* Utils.swift in Sources */, 02025AEC2988229800E694E7 /* AppTrackingProtectionPacketTunnelProvider.swift in Sources */, 02025B1029884DC500E694E7 /* AppTrackerDataParser.swift in Sources */, + EEFC6A602AC0F2F80065027D /* UserText.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6677,6 +6704,7 @@ 85F21DC621145DD5002631A6 /* global.swift in Sources */, F41C2DA326C1925700F9A760 /* BookmarksAndFolders.xcdatamodeld in Sources */, F4F6DFBA26EFF28A00ED7E12 /* BookmarkObjects.swift in Sources */, + EE7A92872AC6DE4700832A36 /* NetworkProtectionNotificationIdentifier.swift in Sources */, 836A941D247F23C600BF8EF5 /* UserAgentManager.swift in Sources */, 4B83397329AFB8D2003F7EA9 /* AppTrackingProtectionFeedbackModel.swift in Sources */, 85CA53A824BB343700A6288C /* Favicons.swift in Sources */, @@ -8928,7 +8956,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 80.2.1; + version = 80.4.1; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3d44a3e892..f903a9e2ac 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/DuckDuckGo/BrowserServicesKit", "state": { "branch": null, - "revision": "e955f958201a91ef353c55236361e095357e9505", - "version": "80.2.1" + "revision": "9dea0583dc6269971fb4728bd3efa1ed53f88306", + "version": "80.4.1" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 9f347056c7..86ca911d04 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -87,7 +87,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Configuration.setURLProvider(AppConfigurationURLProvider()) CrashCollection.start { - Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: $0, includedParameters: [.appVersion]) + Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: $0, includedParameters: []) } clearTmp() @@ -677,9 +677,16 @@ extension AppDelegate: UNUserNotificationCenterDelegate { didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if response.actionIdentifier == UNNotificationDefaultActionIdentifier { - if response.notification.request.identifier == WindowsBrowserWaitlist.notificationIdentitier { + let identifier = response.notification.request.identifier + if identifier == WindowsBrowserWaitlist.notificationIdentitier { presentWindowsBrowserWaitlistSettingsModal() } + +#if NETWORK_PROTECTION + if NetworkProtectionNotificationIdentifier(rawValue: identifier) != nil { + presentNetworkProtectionStatusSettingsModal() + } +#endif } completionHandler() @@ -689,7 +696,14 @@ extension AppDelegate: UNUserNotificationCenterDelegate { let waitlistViewController = WindowsWaitlistViewController(nibName: nil, bundle: nil) presentSettings(with: waitlistViewController) } - + +#if NETWORK_PROTECTION + private func presentNetworkProtectionStatusSettingsModal() { + let networkProtectionRoot = NetworkProtectionRootViewController() + presentSettings(with: networkProtectionRoot) + } +#endif + private func presentSettings(with viewController: UIViewController) { guard let window = window, let rootViewController = window.rootViewController as? MainViewController else { return } @@ -703,5 +717,4 @@ extension AppDelegate: UNUserNotificationCenterDelegate { navigationController?.pushViewController(viewController, animated: true) } } - } diff --git a/DuckDuckGo/AutofillListItemTableViewCell.swift b/DuckDuckGo/AutofillListItemTableViewCell.swift index f531ff4aa6..209ed903d8 100644 --- a/DuckDuckGo/AutofillListItemTableViewCell.swift +++ b/DuckDuckGo/AutofillListItemTableViewCell.swift @@ -118,10 +118,6 @@ class AutofillListItemTableViewCell: UITableViewCell { contentStackView.bottomAnchor.constraint(equalTo: margins.bottomAnchor) ]) } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - } private func setupContentView(with item: AutofillLoginListItemViewModel) { titleLabel.text = item.title diff --git a/DuckDuckGo/DaxDialogs.swift b/DuckDuckGo/DaxDialogs.swift index b26f149fc9..5e346d62c1 100644 --- a/DuckDuckGo/DaxDialogs.swift +++ b/DuckDuckGo/DaxDialogs.swift @@ -73,7 +73,7 @@ final class DaxDialogs { case .siteIsMajorTracker, .siteOwnedByMajorTracker: settings.browsingMajorTrackingSiteShown = flag settings.browsingWithoutTrackersShown = flag - } + } } struct BrowsingSpec: Equatable { diff --git a/DuckDuckGo/KeychainItemsDebugViewController.swift b/DuckDuckGo/KeychainItemsDebugViewController.swift index dc46a1cb80..8caa29ec13 100644 --- a/DuckDuckGo/KeychainItemsDebugViewController.swift +++ b/DuckDuckGo/KeychainItemsDebugViewController.swift @@ -115,10 +115,6 @@ private enum SecClass: CaseIterable { class KeychainItemsDebugViewController: UITableViewController { - override func viewDidLoad() { - super.viewDidLoad() - } - override func numberOfSections(in tableView: UITableView) -> Int { return SecClass.allCases.count } diff --git a/DuckDuckGo/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtectionDebugUtilities.swift index 83062a1b3e..a3d47161b9 100644 --- a/DuckDuckGo/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtectionDebugUtilities.swift @@ -37,6 +37,46 @@ final class NetworkProtectionDebugUtilities { try? activeSession.sendProviderMessage(.expireRegistrationKey) } + + // MARK: - Notifications + + func sendTestNotificationRequest() async throws { + guard let activeSession = try? await ConnectionSessionUtilities.activeSession() else { + return + } + + try? activeSession.sendProviderMessage(.triggerTestNotification) + } + + // MARK: - Failure Simulation + + func triggerSimulation(_ option: NetworkProtectionSimulationOption) async { + guard let activeSession = try? await ConnectionSessionUtilities.activeSession() else { + return + } + + guard let message = option.extensionMessage else { + return + } + try? activeSession.sendProviderMessage(message) + } +} + +private extension NetworkProtectionSimulationOption { + var extensionMessage: ExtensionMessage? { + switch self { + case .crashFatalError: + return .simulateTunnelFatalError + case .crashMemory: + return .simulateTunnelMemoryOveruse + case .tunnelFailure: + return .simulateTunnelFailure + case .controllerFailure: + return nil + case .connectionInterruption: + return .simulateConnectionInterruption + } + } } #endif diff --git a/DuckDuckGo/NetworkProtectionDebugViewController.swift b/DuckDuckGo/NetworkProtectionDebugViewController.swift index f3210ba03f..ffa9d42df0 100644 --- a/DuckDuckGo/NetworkProtectionDebugViewController.swift +++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift @@ -34,7 +34,8 @@ final class NetworkProtectionDebugViewController: UITableViewController { Sections.keychain: "Keychain", Sections.debugFeature: "Debug Features", Sections.simulateFailure: "Simulate Failure", - Sections.registrationKey: "Registration Key" + Sections.registrationKey: "Registration Key", + Sections.notifications: "Notifications" ] @@ -44,6 +45,7 @@ final class NetworkProtectionDebugViewController: UITableViewController { case debugFeature case simulateFailure case registrationKey + case notifications } @@ -63,7 +65,7 @@ final class NetworkProtectionDebugViewController: UITableViewController { case controllerFailure case crashFatalError case crashMemory - + case connectionInterruption } enum RegistrationKeyRows: Int, CaseIterable { @@ -72,6 +74,12 @@ final class NetworkProtectionDebugViewController: UITableViewController { } + enum NotificationsRows: Int, CaseIterable { + + case triggerTestNotification + + } + private let debugFeatures: NetworkProtectionDebugFeatures private let tokenStore: NetworkProtectionTokenStore @@ -122,6 +130,9 @@ final class NetworkProtectionDebugViewController: UITableViewController { case .registrationKey: configure(cell, forRegistrationKeyRow: indexPath.row) + case .notifications: + configure(cell, forNotificationRow: indexPath.row) + case.none: break } @@ -135,6 +146,7 @@ final class NetworkProtectionDebugViewController: UITableViewController { case .debugFeature: return DebugFeatureRows.allCases.count case .simulateFailure: return SimulateFailureRows.allCases.count case .registrationKey: return RegistrationKeyRows.allCases.count + case .notifications: return NotificationsRows.allCases.count case .none: return 0 } @@ -153,6 +165,8 @@ final class NetworkProtectionDebugViewController: UITableViewController { didSelectSimulateFailure(at: indexPath) case .registrationKey: didSelectRegistationKeyAction(at: indexPath) + case .notifications: + didSelectTestNotificationAction(at: indexPath) case .none: break } @@ -172,6 +186,8 @@ final class NetworkProtectionDebugViewController: UITableViewController { cell.textLabel?.text = "Tunnel: Crash (Fatal Error)" case .crashMemory: cell.textLabel?.text = "Tunnel: Crash (CPU/Memory)" + case .connectionInterruption: + cell.textLabel?.text = "Connection Interruption" case .none: break } @@ -179,11 +195,24 @@ final class NetworkProtectionDebugViewController: UITableViewController { private func didSelectSimulateFailure(at indexPath: IndexPath) { switch SimulateFailureRows(rawValue: indexPath.row) { - case .controllerFailure: simulateFailure(option: .controllerFailure) - case .tunnelFailure: simulateFailure(option: .tunnelFailure) - case .crashFatalError: simulateFailure(option: .crashFatalError) - case .crashMemory: simulateFailure(option: .crashMemory) - case .none: return + case .controllerFailure: + NetworkProtectionTunnelController.shouldSimulateFailure = true + case .tunnelFailure: + triggerSimulation(.tunnelFailure) + case .crashFatalError: + triggerSimulation(.crashFatalError) + case .crashMemory: + triggerSimulation(.crashMemory) + case .connectionInterruption: + triggerSimulation(.connectionInterruption) + case .none: + break + } + } + + private func triggerSimulation(_ option: NetworkProtectionSimulationOption) { + Task { + await NetworkProtectionDebugUtilities().triggerSimulation(option) } } @@ -236,22 +265,32 @@ final class NetworkProtectionDebugViewController: UITableViewController { } } - // MARK: Selection Actions + // MARK: Notifications - private func clearAuthToken() { - try? tokenStore.deleteToken() + private func configure(_ cell: UITableViewCell, forNotificationRow row: Int) { + switch NotificationsRows(rawValue: row) { + case .triggerTestNotification: + cell.textLabel?.text = "Test Notification" + case .none: + break + } } - private func simulateControllerFailure() { - NetworkProtectionTunnelController.enabledSimulationOption = .controllerFailure + private func didSelectTestNotificationAction(at indexPath: IndexPath) { + switch NotificationsRows(rawValue: indexPath.row) { + case .triggerTestNotification: + Task { + try await NetworkProtectionDebugUtilities().sendTestNotificationRequest() + } + case .none: + break + } } - private func simulaterTunnelFailure() { - NetworkProtectionTunnelController.enabledSimulationOption = .crashFatalError - } + // MARK: Selection Actions - private func simulateFailure(option: NetworkProtectionSimulationOption) { - NetworkProtectionTunnelController.enabledSimulationOption = .crashMemory + private func clearAuthToken() { + try? tokenStore.deleteToken() } } diff --git a/DuckDuckGo/NetworkProtectionRootViewController.swift b/DuckDuckGo/NetworkProtectionRootViewController.swift index ff37f96256..485f586f5d 100644 --- a/DuckDuckGo/NetworkProtectionRootViewController.swift +++ b/DuckDuckGo/NetworkProtectionRootViewController.swift @@ -23,7 +23,7 @@ import SwiftUI final class NetworkProtectionRootViewController: UIHostingController { - init(inviteCompletion: @escaping () -> Void) { + init(inviteCompletion: @escaping () -> Void = { }) { let rootView = NetworkProtectionRootView(inviteCompletion: inviteCompletion) super.init(rootView: rootView) } diff --git a/DuckDuckGo/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift index d53263188a..6a5bef318b 100644 --- a/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -26,8 +26,7 @@ import NetworkExtension import NetworkProtection final class NetworkProtectionTunnelController: TunnelController { - static var simulationOptions = NetworkProtectionSimulationOptions() - static var enabledSimulationOption: NetworkProtectionSimulationOption? + static var shouldSimulateFailure: Bool = false private let debugFeatures = NetworkProtectionDebugFeatures() private let tokenStore = NetworkProtectionKeychainTokenStore() @@ -98,19 +97,14 @@ final class NetworkProtectionTunnelController: TunnelController { private func start(_ tunnelManager: NETunnelProviderManager) throws { var options = [String: NSObject]() - if Self.simulationOptions.isEnabled(.controllerFailure) { - Self.simulationOptions.setEnabled(false, option: .controllerFailure) + if Self.shouldSimulateFailure { + Self.shouldSimulateFailure = false throw StartError.simulateControllerFailureError } options["activationAttemptId"] = UUID().uuidString as NSString options["authToken"] = try tokenStore.fetchToken() as NSString? - if let optionKey = Self.enabledSimulationOption?.optionKey { - options[optionKey] = NSNumber(value: true) - Self.enabledSimulationOption = nil - } - do { try tunnelManager.connection.startVPNTunnel(options: options) } catch { @@ -227,19 +221,4 @@ final class NetworkProtectionTunnelController: TunnelController { } } -private extension NetworkProtectionSimulationOption { - var optionKey: String? { - switch self { - case .crashFatalError: - return NetworkProtectionOptionKey.tunnelFatalErrorCrashSimulation - case .crashMemory: - return NetworkProtectionOptionKey.tunnelMemoryCrashSimulation - case .tunnelFailure: - return NetworkProtectionOptionKey.tunnelFailureSimulation - default: - return nil - } - } -} - #endif diff --git a/DuckDuckGo/OnboardingDefaultBroswerViewController.swift b/DuckDuckGo/OnboardingDefaultBroswerViewController.swift index addf162132..beb730753b 100644 --- a/DuckDuckGo/OnboardingDefaultBroswerViewController.swift +++ b/DuckDuckGo/OnboardingDefaultBroswerViewController.swift @@ -41,7 +41,4 @@ class OnboardingDefaultBroswerViewController: OnboardingContentViewController { super.onContinuePressed(navigationHandler: navigationHandler) } - override func onSkipPressed(navigationHandler: @escaping () -> Void) { - super.onSkipPressed(navigationHandler: navigationHandler) - } } diff --git a/DuckDuckGo/SaveLoginViewController.swift b/DuckDuckGo/SaveLoginViewController.swift index 54ae7752a6..82d0afec9e 100644 --- a/DuckDuckGo/SaveLoginViewController.swift +++ b/DuckDuckGo/SaveLoginViewController.swift @@ -61,10 +61,6 @@ class SaveLoginViewController: UIViewController { viewModel?.viewControllerDidAppear() } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) diff --git a/DuckDuckGo/SettingsViewController.swift b/DuckDuckGo/SettingsViewController.swift index ade8af04be..1d461443d2 100644 --- a/DuckDuckGo/SettingsViewController.swift +++ b/DuckDuckGo/SettingsViewController.swift @@ -421,7 +421,7 @@ class SettingsViewController: UITableViewController { // This will be tidied up as part of https://app.asana.com/0/0/1205084446087078/f let rootViewController = NetworkProtectionRootViewController { [weak self] in self?.navigationController?.popViewController(animated: true) - let newRootViewController = NetworkProtectionRootViewController { } + let newRootViewController = NetworkProtectionRootViewController() self?.pushNetP(newRootViewController) } pushNetP(rootViewController) @@ -545,10 +545,6 @@ class SettingsViewController: UITableViewController { } } - override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { - return super.tableView(tableView, titleForFooterInSection: section) - } - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let rows = super.tableView(tableView, numberOfRowsInSection: section) if section == appearanceSectionIndex && textSizeCell.isHidden { diff --git a/DuckDuckGoTests/PrivacyIconLogicTests.swift b/DuckDuckGoTests/PrivacyIconLogicTests.swift index ad8c995326..a14220bdc8 100644 --- a/DuckDuckGoTests/PrivacyIconLogicTests.swift +++ b/DuckDuckGoTests/PrivacyIconLogicTests.swift @@ -33,14 +33,6 @@ class PrivacyIconLogicTests: XCTestCase { static let ddgMainURL = URL(string: "https://duckduckgo.com")! static let ddgSupportURL = URL(string: "https://duckduckgo.com/email/settings/support")! - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - func testPrivacyIconIsShieldForPageURL() { let url = PrivacyIconLogicTests.insecurePageURL let icon = PrivacyIconLogic.privacyIcon(for: url) diff --git a/DuckDuckGoTests/TrackerAnimationLogicTests.swift b/DuckDuckGoTests/TrackerAnimationLogicTests.swift index b94ce01f99..07302fc11f 100644 --- a/DuckDuckGoTests/TrackerAnimationLogicTests.swift +++ b/DuckDuckGoTests/TrackerAnimationLogicTests.swift @@ -30,14 +30,6 @@ class TrackerAnimationLogicTests: XCTestCase { static let pageURL = URL(string: "https://example.com")! - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - func testAnimationLogicToAnimateTrackersIfAnyBlocked() { let trackerInfo = makeBlockedTrackerInfo(pageURL: Self.pageURL) XCTAssertTrue(TrackerAnimationLogic.shouldAnimateTrackers(for: trackerInfo)) diff --git a/IntegrationTests/AtbServerTests.swift b/IntegrationTests/AtbServerTests.swift index fe395bac42..8d7a50a7bc 100644 --- a/IntegrationTests/AtbServerTests.swift +++ b/IntegrationTests/AtbServerTests.swift @@ -37,11 +37,7 @@ class AtbServerTests: XCTestCase { loader = StatisticsLoader(statisticsStore: store) } - - override func tearDown() { - super.tearDown() - } - + func testExtiCall() { let waitForCompletion = expectation(description: "wait for completion") diff --git a/PacketTunnelProvider/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift similarity index 96% rename from PacketTunnelProvider/NetworkProtectionPacketTunnelProvider.swift rename to PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 3132cabffc..ccb15449ae 100644 --- a/PacketTunnelProvider/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -166,7 +166,9 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: .dataProtection(.unspecified), errorEvents: nil) let errorStore = NetworkProtectionTunnelErrorStore() - super.init(notificationsPresenter: DefaultNotificationPresenter(), + let notificationsPresenter = NetworkProtectionUNNotificationPresenter() + notificationsPresenter.requestAuthorization() + super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: NetworkProtectionTunnelHealthStore(), controllerErrorStore: errorStore, keychainType: .dataProtection(.unspecified), @@ -201,21 +203,3 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { } } } - -final class DefaultNotificationPresenter: NetworkProtectionNotificationsPresenter { - - func showTestNotification() { - } - - func showReconnectedNotification() { - } - - func showReconnectingNotification() { - } - - func showConnectionFailureNotification() { - } - - func showSupersededNotification() { - } -} diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionUNNotificationPresenter.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionUNNotificationPresenter.swift new file mode 100644 index 0000000000..7ada176e81 --- /dev/null +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionUNNotificationPresenter.swift @@ -0,0 +1,114 @@ +// +// NetworkProtectionUNNotificationPresenter.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import NetworkProtection +import Core + +/// This class takes care of requesting the presentation of notifications using UNNotificationCenter +/// +final class NetworkProtectionUNNotificationPresenter: NSObject, NetworkProtectionNotificationsPresenter { + + private let userNotificationCenter: UNUserNotificationCenter + + private var threadIdentifier: String { + let bundleId = Bundle(for: Self.self).bundleIdentifier ?? "com.duckduckgo.mobile.ios.NetworkExtension" + return bundleId + ".threadIdentifier" + } + + init(userNotificationCenter: UNUserNotificationCenter = .current()) { + self.userNotificationCenter = userNotificationCenter + + super.init() + } + + // MARK: - Setup + + func requestAuthorization() { + userNotificationCenter.delegate = self + requestAlertAuthorization() + } + + // MARK: - Notification Utility methods + + private func requestAlertAuthorization(completionHandler: ((Bool) -> Void)? = nil) { + let options: UNAuthorizationOptions = .alert + + userNotificationCenter.requestAuthorization(options: options) { authorized, _ in + completionHandler?(authorized) + } + } + + private func notificationContent(body: String) -> UNNotificationContent { + let content = UNMutableNotificationContent() + + content.threadIdentifier = threadIdentifier + content.title = UserText.networkProtectionNotificationsTitle + content.body = body + + if #available(iOSApplicationExtension 15.0, *) { + content.interruptionLevel = .timeSensitive + content.relevanceScore = 0 + } + + return content + } + + func showTestNotification() { + // Debug only string. Doesn't need localized + let content = notificationContent(body: "Test notification") + showNotification(.test, content) + } + + func showReconnectedNotification() { + let content = notificationContent(body: UserText.networkProtectionConnectionSuccessNotificationBody) + showNotification(.reconnected, content) + } + + func showReconnectingNotification() { + let content = notificationContent(body: UserText.networkProtectionConnectionInterruptedNotificationBody) + showNotification(.reconnecting, content) + } + + func showConnectionFailureNotification() { + let content = notificationContent(body: UserText.networkProtectionConnectionFailureNotificationBody) + showNotification(.connectionFailure, content) + } + + func showSupersededNotification() { + } + + private func showNotification(_ identifier: NetworkProtectionNotificationIdentifier, _ content: UNNotificationContent) { + let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: .none) + + requestAlertAuthorization { authorized in + guard authorized else { + return + } + self.userNotificationCenter.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue]) + self.userNotificationCenter.add(request) + } + } +} + +extension NetworkProtectionUNNotificationPresenter: UNUserNotificationCenterDelegate { + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + return .banner + } +} diff --git a/PacketTunnelProvider/UserText.swift b/PacketTunnelProvider/UserText.swift new file mode 100644 index 0000000000..0d2bd15fae --- /dev/null +++ b/PacketTunnelProvider/UserText.swift @@ -0,0 +1,35 @@ +// +// UserText.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// swiftlint:disable line_length +final class UserText { + + // MARK: - Network Protection Notifications + + static let networkProtectionNotificationsTitle = NSLocalizedString("network.protection.notification.title", value: "DuckDuckGo", comment: "The title of the notifications shown from Network Protection") + + static let networkProtectionConnectionSuccessNotificationBody = NSLocalizedString("network.protection.success.notification.body", value: "Network Protection is On. Your location and online activity are protected.", comment: "The body of the notification shown when Network Protection reconnects successfully") + + static let networkProtectionConnectionInterruptedNotificationBody = NSLocalizedString("network.protection.interrupted.notification.body", value: "Network Protection was interrupted. Attempting to reconnect now...", comment: "The body of the notification shown when Network Protection's connection is interrupted") + + static let networkProtectionConnectionFailureNotificationBody = NSLocalizedString("network.protection.failure.notification.body", value: "Network Protection failed to connect. Please try again later.", comment: "The body of the notification shown when Network Protection fails to reconnect") +} +// swiftlint:enable line_length