From d89bd599b4087aca0a27a50692667c85e3484279 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 15 Nov 2023 18:42:35 -0800 Subject: [PATCH] Extract NetP access management out into its own type. --- DuckDuckGo.xcodeproj/project.pbxproj | 8 ++ DuckDuckGo/AppDelegate.swift | 22 +--- .../NetworkProtectionAccessController.swift | 121 ++++++++++++++++++ ...orkProtectionTermsAndConditionsStore.swift | 32 +++++ DuckDuckGo/SettingsViewController.swift | 4 +- DuckDuckGo/VPNWaitlist.swift | 46 +------ .../VPNWaitlistDebugViewController.swift | 3 +- DuckDuckGo/VPNWaitlistView.swift | 4 +- DuckDuckGo/VPNWaitlistViewController.swift | 3 +- 9 files changed, 176 insertions(+), 67 deletions(-) create mode 100644 DuckDuckGo/NetworkProtectionAccessController.swift create mode 100644 DuckDuckGo/NetworkProtectionTermsAndConditionsStore.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index aac25e302c..4e11474409 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -303,6 +303,8 @@ 4BBBBA922B03291700D965DA /* VPNWaitlistUserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBBBA912B03291700D965DA /* VPNWaitlistUserText.swift */; }; 4BC21A2F27238B7500229F0E /* RunLoopExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */; }; 4BC6DD1C2A60E6AD001EC129 /* ReportBrokenSiteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6DD1B2A60E6AD001EC129 /* ReportBrokenSiteView.swift */; }; + 4BCD14632B05AF2B000B1E4C /* NetworkProtectionAccessController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD14622B05AF2B000B1E4C /* NetworkProtectionAccessController.swift */; }; + 4BCD14672B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */; }; 4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE27566272F878F006B20B0 /* URLRequestExtension.swift */; }; 4BEF65692989C2FC00B650CB /* AdapterSocketEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D307A2989C0C400918636 /* AdapterSocketEvent.swift */; }; 4BEF656A2989C2FC00B650CB /* ProxyServerEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D307C2989C0C600918636 /* ProxyServerEvent.swift */; }; @@ -1325,6 +1327,8 @@ 4BBBBA912B03291700D965DA /* VPNWaitlistUserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWaitlistUserText.swift; sourceTree = ""; }; 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunLoopExtensionTests.swift; sourceTree = ""; }; 4BC6DD1B2A60E6AD001EC129 /* ReportBrokenSiteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportBrokenSiteView.swift; sourceTree = ""; }; + 4BCD14622B05AF2B000B1E4C /* NetworkProtectionAccessController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAccessController.swift; sourceTree = ""; }; + 4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTermsAndConditionsStore.swift; sourceTree = ""; }; 4BE27566272F878F006B20B0 /* URLRequestExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLRequestExtension.swift; path = ../DuckDuckGo/URLRequestExtension.swift; sourceTree = ""; }; 4BFB911A29B7D9530014D4B7 /* AppTrackingProtectionStoringModelPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionStoringModelPerformanceTests.swift; sourceTree = ""; }; 56244C1C2A137B1900EDF259 /* WaitlistViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistViews.swift; sourceTree = ""; }; @@ -4453,6 +4457,8 @@ EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */, EE458D0C2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift */, EE9D68DB2AE16AE100B55EF4 /* NotificationsAuthorizationController.swift */, + 4BCD14622B05AF2B000B1E4C /* NetworkProtectionAccessController.swift */, + 4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */, ); name = Helpers; sourceTree = ""; @@ -6323,6 +6329,7 @@ 85DFEDED24C7CCA500973FE7 /* AppWidthObserver.swift in Sources */, 4B6484F327FD1E350050A7A1 /* MenuControllerView.swift in Sources */, 1EE7C299294227EC0026C8CB /* AutoconsentSettingsViewController.swift in Sources */, + 4BCD14632B05AF2B000B1E4C /* NetworkProtectionAccessController.swift in Sources */, 1E8AD1D527C2E22900ABA377 /* DownloadsListSectionViewModel.swift in Sources */, 4BC6DD1C2A60E6AD001EC129 /* ReportBrokenSiteView.swift in Sources */, 31584616281AFB46004ADB8B /* AutofillLoginDetailsViewController.swift in Sources */, @@ -6471,6 +6478,7 @@ 85EE7F572246685B000FE757 /* WebContainerViewController.swift in Sources */, 1EC458462948932500CB2B13 /* UIHostingControllerExtension.swift in Sources */, 1E4DCF4E27B6A69600961E25 /* DownloadsListHostingController.swift in Sources */, + 4BCD14672B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift in Sources */, 020108A129A5610C00644F9D /* AppTPActivityHostingViewController.swift in Sources */, C1F341C92A6926920032057B /* EmailAddressPromptViewController.swift in Sources */, 02025B0F29884DC500E694E7 /* AppTrackerDataParser.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 7efe6ba800..836ebc80fc 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -325,7 +325,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { #if NETWORK_PROTECTION widgetRefreshModel.beginObservingVPNStatus() - updateVPNAccessFromFeatureFlagState() + NetworkProtectionAccessController().refreshNetworkProtectionAccess() #endif return true @@ -798,26 +798,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } catch {} } } - - func updateVPNAccessFromFeatureFlagState() { - guard NetworkProtectionKeychainTokenStore().isFeatureActivated else { - return - } - - let waitlistStorage = VPNWaitlist.shared.waitlistStorage - let configManager = ContentBlocking.shared.privacyConfigurationManager - - if !configManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlistBetaActive) { - waitlistStorage.deleteWaitlistState() - try? NetworkProtectionKeychainTokenStore().deleteToken() - - Task { - let controller = NetworkProtectionTunnelController() - await controller.stop() - await controller.removeVPN() - } - } - } #endif } diff --git a/DuckDuckGo/NetworkProtectionAccessController.swift b/DuckDuckGo/NetworkProtectionAccessController.swift new file mode 100644 index 0000000000..8083482cf9 --- /dev/null +++ b/DuckDuckGo/NetworkProtectionAccessController.swift @@ -0,0 +1,121 @@ +// +// NetworkProtectionAccessController.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 +import BrowserServicesKit +import ContentBlocking +import Core +import NetworkProtection +import Waitlist + +enum NetworkProtectionAccessType { + /// Used if the user does not have waitlist feature flag access + case none + + /// Used if the user has waitlist feature flag access, but has not joined the waitlist + case waitlistAvailable + + /// Used if the user has waitlist feature flag access, and has joined the waitlist + case waitlistJoined + + /// Used if the user has been invited via the waitlist, but needs to accept the Privacy Policy and Terms of Service + case waitlistInvitedPendingTermsAcceptance + + /// Used if the user has been invited via the waitlist and has accepted the Privacy Policy and Terms of Service + case waitlistInvited + + /// Used if the user has been invited to test Network Protection directly + case inviteCodeInvited +} + +protocol NetworkProtectionAccess { + func networkProtectionAccessType() -> NetworkProtectionAccessType +} + +struct NetworkProtectionAccessController: NetworkProtectionAccess { + + private let networkProtectionActivation: NetworkProtectionFeatureActivation + private let networkProtectionWaitlistStorage: WaitlistStorage + private let networkProtectionTermsAndConditionsStore: NetworkProtectionTermsAndConditionsStore + private let privacyConfigurationManager: PrivacyConfigurationManaging + + init( + networkProtectionActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), + networkProtectionWaitlistStorage: WaitlistStorage = VPNWaitlist.shared.waitlistStorage, + networkProtectionTermsAndConditionsStore: NetworkProtectionTermsAndConditionsStore = NetworkProtectionTermsAndConditionsUserDefaultsStore(), + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager + ) { + self.networkProtectionActivation = networkProtectionActivation + self.networkProtectionWaitlistStorage = networkProtectionWaitlistStorage + self.networkProtectionTermsAndConditionsStore = networkProtectionTermsAndConditionsStore + self.privacyConfigurationManager = privacyConfigurationManager + } + + func networkProtectionAccessType() -> NetworkProtectionAccessType { + // First, check for users who have activated the VPN via an invite code: + if networkProtectionActivation.isFeatureActivated && !networkProtectionWaitlistStorage.isInvited { + return .inviteCodeInvited + } + + // Next, check if the waitlist is still active; if not, the user has no access. + let isWaitlistActive = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlistBetaActive) + if !isWaitlistActive { + return .none + } + + // Next, check if a waitlist user has NetP access and whether they need to accept T&C. + if networkProtectionActivation.isFeatureActivated && networkProtectionWaitlistStorage.isInvited { + if networkProtectionTermsAndConditionsStore.networkProtectionWaitlistTermsAndConditionsAccepted { + return .waitlistInvited + } else { + return .waitlistInvitedPendingTermsAcceptance + } + } + + // Next, check if the user has waitlist access at all and whether they've already joined. + let hasWaitlistAccess = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlist) + if hasWaitlistAccess { + if networkProtectionWaitlistStorage.isOnWaitlist { + return .waitlistJoined + } else { + return .waitlistAvailable + } + } + + return .none + } + + func refreshNetworkProtectionAccess() { + guard networkProtectionActivation.isFeatureActivated else { + return + } + + if !privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlistBetaActive) { + networkProtectionWaitlistStorage.deleteWaitlistState() + try? NetworkProtectionKeychainTokenStore().deleteToken() + + Task { + let controller = NetworkProtectionTunnelController() + await controller.stop() + await controller.removeVPN() + } + } + } + +} diff --git a/DuckDuckGo/NetworkProtectionTermsAndConditionsStore.swift b/DuckDuckGo/NetworkProtectionTermsAndConditionsStore.swift new file mode 100644 index 0000000000..46a6c024e2 --- /dev/null +++ b/DuckDuckGo/NetworkProtectionTermsAndConditionsStore.swift @@ -0,0 +1,32 @@ +// +// NetworkProtectionTermsAndConditionsStore.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 +import Core + +protocol NetworkProtectionTermsAndConditionsStore { + var networkProtectionWaitlistTermsAndConditionsAccepted: Bool { get set } +} + +struct NetworkProtectionTermsAndConditionsUserDefaultsStore: NetworkProtectionTermsAndConditionsStore { + + @UserDefaultsWrapper(key: .networkProtectionWaitlistTermsAndConditionsAccepted, defaultValue: false) + var networkProtectionWaitlistTermsAndConditionsAccepted: Bool + +} diff --git a/DuckDuckGo/SettingsViewController.swift b/DuckDuckGo/SettingsViewController.swift index ac1211b5b5..09c25f3776 100644 --- a/DuckDuckGo/SettingsViewController.swift +++ b/DuckDuckGo/SettingsViewController.swift @@ -368,7 +368,7 @@ class SettingsViewController: UITableViewController { #if NETWORK_PROTECTION private func updateNetPCellSubtitle(connectionStatus: ConnectionStatus) { - switch VPNWaitlist.shared.networkProtectionAccessType { + switch NetworkProtectionAccessController().networkProtectionAccessType() { case .none, .waitlistAvailable, .waitlistJoined, .waitlistInvitedPendingTermsAcceptance: netPCell.detailTextLabel?.text = VPNWaitlist.shared.settingsSubtitle case .waitlistInvited, .inviteCodeInvited: @@ -435,7 +435,7 @@ class SettingsViewController: UITableViewController { #if NETWORK_PROTECTION @available(iOS 15, *) private func showNetP() { - switch VPNWaitlist.shared.networkProtectionAccessType { + switch NetworkProtectionAccessController().networkProtectionAccessType() { case .inviteCodeInvited, .waitlistInvited: // This will be tidied up as part of https://app.asana.com/0/0/1205084446087078/f let rootViewController = NetworkProtectionRootViewController { [weak self] in diff --git a/DuckDuckGo/VPNWaitlist.swift b/DuckDuckGo/VPNWaitlist.swift index 4870a82be5..5c567337e1 100644 --- a/DuckDuckGo/VPNWaitlist.swift +++ b/DuckDuckGo/VPNWaitlist.swift @@ -66,55 +66,19 @@ final class VPNWaitlist: Waitlist { return false } - @UserDefaultsWrapper(key: .networkProtectionWaitlistTermsAndConditionsAccepted, defaultValue: false) - static var termsAndConditionsAccepted: Bool - - var networkProtectionAccessType: AccessType { - let authTokenStore = NetworkProtectionKeychainTokenStore() - - // First, check for users who have activated the VPN via an invite code: - if authTokenStore.isFeatureActivated && !waitlistStorage.isInvited { - return .inviteCodeInvited - } - - // Next, check if the waitlist is still active; if not, the user has no access. - let isWaitlistActive = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlistBetaActive) - if !isWaitlistActive { - return .none - } - - // Next, check if a waitlist user has NetP access and whether they need to accept T&C. - if authTokenStore.isFeatureActivated && waitlistStorage.isInvited { - if Self.termsAndConditionsAccepted { - return .waitlistInvited - } else { - return .waitlistInvitedPendingTermsAcceptance - } - } - - // Next, check if the user has waitlist access at all and whether they've already joined. - let hasWaitlistAccess = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlist) - if hasWaitlistAccess { - if waitlistStorage.isOnWaitlist { - return .waitlistJoined - } else { - return .waitlistAvailable - } - } - - return .none - } - let waitlistStorage: WaitlistStorage let waitlistRequest: WaitlistRequest private let privacyConfigurationManager: PrivacyConfigurationManaging + private let networkProtectionAccess: NetworkProtectionAccess init(store: WaitlistStorage, request: WaitlistRequest, - privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager) { + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, + networkProtectionAccess: NetworkProtectionAccess = NetworkProtectionAccessController()) { self.waitlistStorage = store self.waitlistRequest = request self.privacyConfigurationManager = privacyConfigurationManager + self.networkProtectionAccess = networkProtectionAccess let hasWaitlistAccess = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlist) let isWaitlistActive = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlistBetaActive) @@ -126,7 +90,7 @@ final class VPNWaitlist: Waitlist { } var settingsSubtitle: String { - switch VPNWaitlist.shared.networkProtectionAccessType { + switch networkProtectionAccess.networkProtectionAccessType() { case .none: return "" case .waitlistAvailable: diff --git a/DuckDuckGo/VPNWaitlistDebugViewController.swift b/DuckDuckGo/VPNWaitlistDebugViewController.swift index fc1bb67b13..2463123e71 100644 --- a/DuckDuckGo/VPNWaitlistDebugViewController.swift +++ b/DuckDuckGo/VPNWaitlistDebugViewController.swift @@ -152,7 +152,8 @@ final class VPNWaitlistDebugViewController: UITableViewController { switch row { case .resetTermsAndConditionsAcceptance: - VPNWaitlist.termsAndConditionsAccepted = false + var termsAndConditionsStore = NetworkProtectionTermsAndConditionsUserDefaultsStore() + termsAndConditionsStore.networkProtectionWaitlistTermsAndConditionsAccepted = false case .scheduleWaitlistNotification: DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) { self.storage.store(inviteCode: "ABCD1234") diff --git a/DuckDuckGo/VPNWaitlistView.swift b/DuckDuckGo/VPNWaitlistView.swift index 1e3d329975..9c757586d4 100644 --- a/DuckDuckGo/VPNWaitlistView.swift +++ b/DuckDuckGo/VPNWaitlistView.swift @@ -87,7 +87,9 @@ struct VPNWaitlistSignUpView: View { .buttonStyle(RoundedButtonStyle(enabled: !requestInFlight)) .padding(.top, 24) - Button(UserText.networkProtectionWaitlistButtonExistingInviteCode, action: { action(.custom(.openNetworkProtectionInviteCodeScreen)) }) + Button(UserText.networkProtectionWaitlistButtonExistingInviteCode, action: { + action(.custom(.openNetworkProtectionInviteCodeScreen)) + }) .buttonStyle(RoundedButtonStyle(enabled: true, style: .bordered)) .padding(.top, 18) diff --git a/DuckDuckGo/VPNWaitlistViewController.swift b/DuckDuckGo/VPNWaitlistViewController.swift index 7e69cdc457..d866432142 100644 --- a/DuckDuckGo/VPNWaitlistViewController.swift +++ b/DuckDuckGo/VPNWaitlistViewController.swift @@ -134,7 +134,8 @@ extension VPNWaitlistViewController: WaitlistViewModelDelegate { } if action == .acceptNetworkProtectionTerms { - VPNWaitlist.termsAndConditionsAccepted = true + var termsAndConditionsStore = NetworkProtectionTermsAndConditionsUserDefaultsStore() + termsAndConditionsStore.networkProtectionWaitlistTermsAndConditionsAccepted = true self.navigationController?.popViewController(animated: true) let networkProtectionViewController = NetworkProtectionRootViewController()