Skip to content

Commit

Permalink
Extract NetP access management out into its own type.
Browse files Browse the repository at this point in the history
  • Loading branch information
samsymons committed Nov 16, 2023
1 parent 534c085 commit d89bd59
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 67 deletions.
8 changes: 8 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1325,6 +1327,8 @@
4BBBBA912B03291700D965DA /* VPNWaitlistUserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWaitlistUserText.swift; sourceTree = "<group>"; };
4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunLoopExtensionTests.swift; sourceTree = "<group>"; };
4BC6DD1B2A60E6AD001EC129 /* ReportBrokenSiteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportBrokenSiteView.swift; sourceTree = "<group>"; };
4BCD14622B05AF2B000B1E4C /* NetworkProtectionAccessController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAccessController.swift; sourceTree = "<group>"; };
4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTermsAndConditionsStore.swift; sourceTree = "<group>"; };
4BE27566272F878F006B20B0 /* URLRequestExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLRequestExtension.swift; path = ../DuckDuckGo/URLRequestExtension.swift; sourceTree = "<group>"; };
4BFB911A29B7D9530014D4B7 /* AppTrackingProtectionStoringModelPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionStoringModelPerformanceTests.swift; sourceTree = "<group>"; };
56244C1C2A137B1900EDF259 /* WaitlistViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistViews.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4453,6 +4457,8 @@
EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */,
EE458D0C2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift */,
EE9D68DB2AE16AE100B55EF4 /* NotificationsAuthorizationController.swift */,
4BCD14622B05AF2B000B1E4C /* NetworkProtectionAccessController.swift */,
4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */,
);
name = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
22 changes: 1 addition & 21 deletions DuckDuckGo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

#if NETWORK_PROTECTION
widgetRefreshModel.beginObservingVPNStatus()
updateVPNAccessFromFeatureFlagState()
NetworkProtectionAccessController().refreshNetworkProtectionAccess()
#endif

return true
Expand Down Expand Up @@ -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

}
Expand Down
121 changes: 121 additions & 0 deletions DuckDuckGo/NetworkProtectionAccessController.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}

}
32 changes: 32 additions & 0 deletions DuckDuckGo/NetworkProtectionTermsAndConditionsStore.swift
Original file line number Diff line number Diff line change
@@ -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

}
4 changes: 2 additions & 2 deletions DuckDuckGo/SettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
46 changes: 5 additions & 41 deletions DuckDuckGo/VPNWaitlist.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -126,7 +90,7 @@ final class VPNWaitlist: Waitlist {
}

var settingsSubtitle: String {
switch VPNWaitlist.shared.networkProtectionAccessType {
switch networkProtectionAccess.networkProtectionAccessType() {
case .none:
return ""
case .waitlistAvailable:
Expand Down
3 changes: 2 additions & 1 deletion DuckDuckGo/VPNWaitlistDebugViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion DuckDuckGo/VPNWaitlistView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion DuckDuckGo/VPNWaitlistViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit d89bd59

Please sign in to comment.