Skip to content

Commit

Permalink
Status view error UI (#1915)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1205084446087081/f
Description:

This is the first PR for the Status View error states. The primary purpose of this is to get the design for review ASAP as this is the last task blocking design review. Therefore the main intention is to show an error.

This is initially achieved by just checking the ConnectionErrorObserver for a non-nil value, showing the generic connection failed error in that case, then showing no error in the case of a nil value.

A following PR will add handling of connection interruptions, however this will require some BSK changes so I didn’t want to block the design review to wait for that sometimes lengthy process.
  • Loading branch information
graeme authored Aug 18, 2023
1 parent 0de03df commit c7369db
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 9 deletions.
7 changes: 7 additions & 0 deletions DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ extension ConnectionStatusObserverThroughSession {
}
}

extension ConnectionErrorObserverThroughSession {
convenience init() {
self.init(platformNotificationCenter: .default,
platformDidWakeNotification: UIApplication.didBecomeActiveNotification)
}
}

extension ConnectionServerInfoObserverThroughSession {
convenience init() {
self.init(platformNotificationCenter: .default,
Expand Down
35 changes: 32 additions & 3 deletions DuckDuckGo/NetworkProtectionDebugViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ import NetworkProtection

final class NetworkProtectionDebugViewController: UITableViewController {
private let titles = [
Sections.keychain: "Keychain"
Sections.keychain: "Keychain",
Sections.simulateFailure: "Simulate Failure"
]

enum Sections: Int, CaseIterable {

case keychain
case simulateFailure

}

Expand Down Expand Up @@ -91,7 +93,17 @@ final class NetworkProtectionDebugViewController: UITableViewController {
break
}

default: break
case .simulateFailure:
switch SimulateFailureRows(rawValue: indexPath.row) {
case .controllerFailure:
cell.textLabel?.text = "Enable NetP > Controller Failure"
case .tunnelFailure:
cell.textLabel?.text = "Enable NetP > Tunnel Failure"
case .none:
break
}
case.none:
break
}

return cell
Expand All @@ -100,7 +112,9 @@ final class NetworkProtectionDebugViewController: UITableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch Sections(rawValue: section) {
case .keychain: return KeychainRows.allCases.count
case .simulateFailure: return SimulateFailureRows.allCases.count
case .none: return 0

}
}

Expand All @@ -113,7 +127,14 @@ final class NetworkProtectionDebugViewController: UITableViewController {
case .clearAuthToken: clearAuthToken()
default: break
}
default: break
case .simulateFailure:
switch SimulateFailureRows(rawValue: indexPath.row) {
case .controllerFailure: simulateControllerFailure()
case .tunnelFailure: simulaterTunnelFailure()
case .none: return
}
case .none:
break
}

tableView.deselectRow(at: indexPath, animated: true)
Expand All @@ -125,5 +146,13 @@ final class NetworkProtectionDebugViewController: UITableViewController {
try? tokenStore.deleteToken()
}

private func simulateControllerFailure() {
NetworkProtectionTunnelController.simulationOptions.setEnabled(true, option: .controllerFailure)
}

private func simulaterTunnelFailure() {
NetworkProtectionTunnelController.simulationOptions.setEnabled(true, option: .tunnelFailure)
}

#endif
}
33 changes: 33 additions & 0 deletions DuckDuckGo/NetworkProtectionStatusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,20 @@ struct NetworkProtectionStatusView: View {

var body: some View {
List {
if let errorItem = statusModel.error {
NetworkProtectionErrorView(
title: errorItem.title,
message: errorItem.message
)
}
toggle()
if statusModel.shouldShowConnectionDetails {
connectionDetails()
}
}
.animation(.default, value: statusModel.shouldShowError)
.padding(.top, statusModel.error == nil ? 0 : -20)
.animation(.default, value: statusModel.shouldShowConnectionDetails)
.applyListStyle()
.navigationTitle(UserText.netPNavTitle)
}
Expand Down Expand Up @@ -80,6 +89,9 @@ struct NetworkProtectionStatusView: View {
.scaledToFit()
.frame(height: 96)
.padding(8)
.if(statusModel.shouldShowError) {
$0.rotationEffect(Angle.degrees(statusModel.shouldShowError ? 180 : 0))
}
Text(statusModel.headerTitle)
.font(.system(size: 17, weight: .semibold))
.multilineTextAlignment(.center)
Expand Down Expand Up @@ -133,6 +145,27 @@ struct NetworkProtectionStatusView: View {
}
}

private struct NetworkProtectionErrorView: View {
let title: String
let message: String

var body: some View {
VStack(alignment: .leading) {
HStack {
Image("Alert-Color-16")
Text(title)
.font(.system(size: 16))
.foregroundColor(.primary)
.bold()
}
Text(message)
.font(.system(size: 16))
.foregroundColor(.primary)
}
.listRowBackground(Color.cellBackground)
}
}

private struct NetworkProtectionServerItemView: View {
let imageID: String
let title: String
Expand Down
32 changes: 31 additions & 1 deletion DuckDuckGo/NetworkProtectionStatusViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,23 @@ final class NetworkProtectionStatusViewModel: ObservableObject {
private let tunnelController: TunnelController
private let statusObserver: ConnectionStatusObserver
private let serverInfoObserver: ConnectionServerInfoObserver
private let errorObserver: ConnectionErrorObserver
private var cancellables: Set<AnyCancellable> = []

// MARK: Error

struct ErrorItem {
let title: String
let message: String
}

@Published public var error: ErrorItem? {
didSet {
shouldShowError = error != nil
}
}
@Published public var shouldShowError: Bool = false

// MARK: Header
@Published public var statusImageID: String
@Published public var headerTitle: String
Expand All @@ -52,10 +67,12 @@ final class NetworkProtectionStatusViewModel: ObservableObject {

public init(tunnelController: TunnelController = NetworkProtectionTunnelController(),
statusObserver: ConnectionStatusObserver = ConnectionStatusObserverThroughSession(),
serverInfoObserver: ConnectionServerInfoObserver = ConnectionServerInfoObserverThroughSession()) {
serverInfoObserver: ConnectionServerInfoObserver = ConnectionServerInfoObserverThroughSession(),
errorObserver: ConnectionErrorObserver = ConnectionErrorObserverThroughSession()) {
self.tunnelController = tunnelController
self.statusObserver = statusObserver
self.serverInfoObserver = serverInfoObserver
self.errorObserver = errorObserver
statusMessage = Self.message(for: statusObserver.recentValue)
self.headerTitle = Self.titleText(connected: statusObserver.recentValue.isConnected)
self.statusImageID = Self.statusImageID(connected: statusObserver.recentValue.isConnected)
Expand All @@ -65,6 +82,19 @@ final class NetworkProtectionStatusViewModel: ObservableObject {
setUpStatusMessagePublishers()
setUpDisableTogglePublisher()
setUpServerInfoPublishers()

errorObserver.publisher
.map {
$0.map { _ in
ErrorItem(
title: UserText.netPStatusViewErrorConnectionFailedTitle,
message: UserText.netPStatusViewErrorConnectionFailedMessage
)
}
}
.receive(on: DispatchQueue.main)
.assign(to: \.error, onWeaklyHeld: self)
.store(in: &cancellables)
}

private func setUpIsConnectedStatePublishers() {
Expand Down
37 changes: 32 additions & 5 deletions DuckDuckGo/NetworkProtectionTunnelController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,40 @@ import NetworkExtension
import NetworkProtection

final class NetworkProtectionTunnelController: TunnelController {
static var simulationOptions = NetworkProtectionSimulationOptions()

private let tokenStore = NetworkProtectionKeychainTokenStore(useSystemKeychain: false, errorEvents: nil)
private let errorStore = NetworkProtectionTunnelErrorStore()

// MARK: - Starting & Stopping the VPN

enum StartError: LocalizedError {
case connectionStatusInvalid
case simulateControllerFailureError
}

/// Starts the VPN connection used for Network Protection
///
func start() async {
do {
try await startWithError()
} catch {
// Will handle this as part of https://app.asana.com/0/0/1205084446087081/f
#if DEBUG
errorStore.lastErrorMessage = error.localizedDescription
#endif
}
}

func stop() async {
do {
try await ConnectionSessionUtilities.activeSession()?.stopVPNTunnel()
} catch {
// Will handle this as part of https://app.asana.com/0/0/1205084446087081/f
#if DEBUG
errorStore.lastErrorMessage = error.localizedDescription
#endif
}
}

private let tokenStore = NetworkProtectionKeychainTokenStore(useSystemKeychain: false, errorEvents: nil)
private let connectionObserver = ConnectionStatusObserverThroughSession()

private func startWithError() async throws {
let tunnelManager: NETunnelProviderManager

Expand Down Expand Up @@ -78,9 +90,24 @@ 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)
throw StartError.simulateControllerFailureError
}

options["activationAttemptId"] = UUID().uuidString as NSString
options["authToken"] = try tokenStore.fetchToken() as NSString?

if Self.simulationOptions.isEnabled(.tunnelFailure) {
Self.simulationOptions.setEnabled(false, option: .tunnelFailure)
options[NetworkProtectionOptionKey.tunnelFailureSimulation] = NetworkProtectionOptionValue.true
}

if Self.simulationOptions.isEnabled(.controllerFailure) {
Self.simulationOptions.setEnabled(false, option: .controllerFailure)
throw StartError.simulateControllerFailureError
}

do {
try tunnelManager.connection.startVPNTunnel(options: options)
} catch {
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,8 @@ In addition to the details entered into this form, your app issue report will co
static let netPStatusViewIPAddress = NSLocalizedString("network.protection.status.view.ip.address", value: "IP Address", comment: "IP Address label shown in NetworkProtection's status view.")
static let netPStatusViewConnectionDetails = NSLocalizedString("network.protection.status.view.connection.details", value: "Connection Details", comment: "Connection details label shown in NetworkProtection's status view.")
static let netPStatusViewShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share Feedback", comment: "The status view 'Share Feedback' button which is shown inline on the status view after the \(netPInviteOnlyMessage) text")
static let netPStatusViewErrorConnectionFailedTitle = NSLocalizedString("network.protection.status.view.error.connection.failed.title", value: "Failed to Connect.", comment: "Generic connection failed error title shown in NetworkProtection's status view.")
static let netPStatusViewErrorConnectionFailedMessage = NSLocalizedString("network.protection.status.view.error.connection.failed.message", value: "Please try again later.", comment: "Generic connection failed error message shown in NetworkProtection's status view.")

static let inviteDialogContinueButton = NSLocalizedString("invite.dialog.continue.button", value: "Continue", comment: "Continue button on an invite dialog")
static let inviteDialogGetStartedButton = NSLocalizedString("invite.dialog.get.started.button", value: "Get Started", comment: "Get Started button on an invite dialog")
Expand Down
6 changes: 6 additions & 0 deletions DuckDuckGo/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,12 @@ https://duckduckgo.com/mac";
/* Connection details label shown in NetworkProtection's status view. */
"network.protection.status.view.connection.details" = "Connection Details";

/* Generic connection failed error message shown in NetworkProtection's status view. */
"network.protection.status.view.error.connection.failed.message" = "Please try again later.";

/* Generic connection failed error title shown in NetworkProtection's status view. */
"network.protection.status.view.error.connection.failed.title" = "Failed to Connect.";

/* IP Address label shown in NetworkProtection's status view. */
"network.protection.status.view.ip.address" = "IP Address";

Expand Down

0 comments on commit c7369db

Please sign in to comment.