From 2595778ed99e4f49d5c8ef002e31018e977b0a54 Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Thu, 5 Oct 2023 14:47:36 +0200 Subject: [PATCH] Add UI for blocked state --- .../TunnelStatusNotificationProvider.swift | 44 ++++++++++++++----- .../TunnelManager/TunnelManager.swift | 35 ++++++++++++--- .../Tunnel/TunnelControlView.swift | 19 ++------ .../BlockedStateErrorMapper.swift | 4 +- 4 files changed, 70 insertions(+), 32 deletions(-) diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift index 87164e0eaa51..d36c41303cba 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift @@ -7,11 +7,12 @@ // import Foundation +import PacketTunnelCore final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotificationProvider { private var isWaitingForConnectivity = false private var noNetwork = false - private var packetTunnelError: String? + private var packetTunnelError: BlockedStateReason? private var tunnelManagerError: Error? private var tunnelObserver: TunnelBlockObserver? @@ -57,7 +58,7 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific private func handleTunnelStatus(_ tunnelStatus: TunnelStatus) { let invalidateForTunnelError: Bool if case let .error(blockStateReason) = tunnelStatus.state { - invalidateForTunnelError = updateLastTunnelError(blockStateReason.rawValue) + invalidateForTunnelError = updateLastTunnelError(blockStateReason) } else { invalidateForTunnelError = updateLastTunnelError(nil) } @@ -71,7 +72,7 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific } } - private func updateLastTunnelError(_ lastTunnelError: String?) -> Bool { + private func updateLastTunnelError(_ lastTunnelError: BlockedStateReason?) -> Bool { if packetTunnelError != lastTunnelError { packetTunnelError = lastTunnelError @@ -120,22 +121,21 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific return false } - private func notificationDescription(for packetTunnelError: String) -> InAppNotificationDescriptor { + private func notificationDescription(for packetTunnelError: BlockedStateReason) -> InAppNotificationDescriptor { InAppNotificationDescriptor( identifier: identifier, style: .error, title: NSLocalizedString( - "TUNNEL_LEAKING_INAPP_NOTIFICATION_TITLE", - value: "NETWORK TRAFFIC MIGHT BE LEAKING", + "TUNNEL_BLOCKED_INAPP_NOTIFICATION_TITLE", + value: "BLOCKING INTERNET", comment: "" ), body: .init(string: String( format: NSLocalizedString( - "PACKET_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY", - value: "Could not configure VPN: %@", + "TUNNEL_BLOCKED_INAPP_NOTIFICATION_BODY", + value: localizedReasonForBlockedStateError(packetTunnelError), comment: "" - ), - packetTunnelError + ) )) ) } @@ -220,4 +220,28 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific ) ) } + + private func localizedReasonForBlockedStateError(_ error: BlockedStateReason) -> String { + let errorString: String + + switch error { + case .outdatedSchema: + errorString = "Unable to start tunnel connection after update. Please send a problem report." + case .noRelaysSatisfyingConstraints: + errorString = "No servers match your settings, try changing server or other settings." + case .invalidAccount: + errorString = "You are logged in with an invalid account number. Please log out and try another one." + case .deviceRevoked, .deviceLoggedOut: + errorString = "Unable to authenticate account. Please log out and log back in." + default: + errorString = "Unable to start tunnel connection. Please send a problem report." + } + + return NSLocalizedString( + "BLOCKED_STATE_ERROR_TITLE", + tableName: "Main", + value: errorString, + comment: "" + ) + } } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 9966b40fc796..1f27c66395de 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -289,6 +289,12 @@ final class TunnelManager: StorePaymentObserver { finish(result.error) } } catch { + if let error = error as? NoRelaysSatisfyingConstraintsError { + _ = self.setTunnelStatus { tunnelStatus in + tunnelStatus.state = .error(.noRelaysSatisfyingConstraints) + } + } + finish(error) return AnyCancellable() @@ -703,22 +709,30 @@ final class TunnelManager: StorePaymentObserver { lastPacketTunnelKeyRotation = newPacketTunnelKeyRotation refreshDeviceState() } - - // TODO: handle blocked state (error state). See how handleRestError() manages invalid account or revoked device. - switch newTunnelStatus.state { case .connecting, .reconnecting: // Start polling tunnel status to keep the relay information up to date // while the tunnel process is trying to connect. startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval) - case .connected, .waitingForConnectivity(.noConnection), .error: + case .connected, .waitingForConnectivity(.noConnection): // Start polling tunnel status to keep connectivity status up to date. startPollingTunnelStatus(interval: establishedTunnelStatusPollInterval) case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork): // Stop polling tunnel status once connection moved to final state. cancelPollingTunnelStatus() + + case let .error(blockedStateReason): + switch blockedStateReason { + case .deviceRevoked, .invalidAccount: + handleBlockedState(reason: blockedStateReason) + default: + break + } + + // Stop polling tunnel status once blocked state has been determined. + cancelPollingTunnelStatus() } DispatchQueue.main.async { @@ -1084,13 +1098,24 @@ final class TunnelManager: StorePaymentObserver { guard let restError = error as? REST.Error else { return } if restError.compareErrorCode(.deviceNotFound) { - setDeviceState(.revoked, persist: true) + handleBlockedState(reason: .deviceRevoked) } else if restError.compareErrorCode(.invalidAccount) { + handleBlockedState(reason: .invalidAccount) + } + } + + private func handleBlockedState(reason: BlockedStateReason) { + switch reason { + case .deviceRevoked: + setDeviceState(.revoked, persist: true) + case .invalidAccount: unsetTunnelConfiguration { self.setDeviceState(.revoked, persist: true) self.operationQueue.cancelAllOperations() self.wipeAllUserData() } + default: + break } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index cf76e00d3e34..dc6669fb4710 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -496,7 +496,7 @@ private extension TunnelState { comment: "" ) - case .waitingForConnectivity(.noConnection): + case .waitingForConnectivity(.noConnection), .error: return NSLocalizedString( "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY", tableName: "Main", @@ -511,10 +511,6 @@ private extension TunnelState { value: "No network", comment: "" ) - - case let .error(blockedStateReason): - // TODO: Fix me - return "" } } @@ -535,17 +531,14 @@ private extension TunnelState { value: "Select location", comment: "" ) - case .connecting, .connected, .reconnecting, .waitingForConnectivity: + + case .connecting, .connected, .reconnecting, .waitingForConnectivity, .error: return NSLocalizedString( "SWITCH_LOCATION_BUTTON_TITLE", tableName: "Main", value: "Switch location", comment: "" ) - - case let .error(blockedStateReason): - // TODO: Fix me - return "" } } @@ -591,7 +584,7 @@ private extension TunnelState { tunnelInfo.location.country ) - case .waitingForConnectivity(.noConnection): + case .waitingForConnectivity(.noConnection), .error: return NSLocalizedString( "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY_ACCESSIBILITY_LABEL", tableName: "Main", @@ -622,10 +615,6 @@ private extension TunnelState { value: "Reconnecting", comment: "" ) - - case let .error(blockedStateReason): - // TODO: Fix me - return "" } } diff --git a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift index a31c508288a6..f7d22d16db2f 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift @@ -17,8 +17,8 @@ import WireGuardKit /** Struct responsible for mapping errors that may occur in the packet tunnel to the `BlockedStateReason`. */ -struct BlockedStateErrorMapper: BlockedStateErrorMapperProtocol { - func mapError(_ error: Error) -> BlockedStateReason { +public struct BlockedStateErrorMapper: BlockedStateErrorMapperProtocol { + public func mapError(_ error: Error) -> BlockedStateReason { switch error { case let error as ReadDeviceDataError: // Such error is thrown by implementations of `SettingsReaderProtocol`.