From 004a5bf1fa62a2cb8c1d049fc779d5d6091a6000 Mon Sep 17 00:00:00 2001 From: mojganii Date: Wed, 15 May 2024 16:50:01 +0200 Subject: [PATCH] Add multi-hop toggle to settings view --- .../QuantumResistanceSettings.swift | 6 +++ .../TunnelSettingsUpdate.swift | 5 ++ .../Classes/AccessbilityIdentifier.swift | 6 +++ .../VPNSettings/CustomDNSDataSource.swift | 2 +- .../VPNSettings/CustomDNSViewController.swift | 18 ++----- .../VPNSettings/VPNSettingsCellFactory.swift | 19 +++++++ .../VPNSettings/VPNSettingsDataSource.swift | 31 +++++++++++ .../VPNSettingsDataSourceDelegate.swift | 7 +++ .../VPNSettingsViewController.swift | 52 +++++++++++++++++-- .../VPNSettings/VPNSettingsViewModel.swift | 6 +++ 10 files changed, 132 insertions(+), 20 deletions(-) diff --git a/ios/MullvadSettings/QuantumResistanceSettings.swift b/ios/MullvadSettings/QuantumResistanceSettings.swift index b5c12ae703b5..fbde4e135142 100644 --- a/ios/MullvadSettings/QuantumResistanceSettings.swift +++ b/ios/MullvadSettings/QuantumResistanceSettings.swift @@ -7,6 +7,12 @@ // import Foundation +// FIXME: Remove MultihopState when upgrading is merged +public enum MultihopState: Codable { + case automatic + case on + case off +} public enum TunnelQuantumResistance: Codable { case automatic diff --git a/ios/MullvadSettings/TunnelSettingsUpdate.swift b/ios/MullvadSettings/TunnelSettingsUpdate.swift index c915e5dc7f5f..7b4fb5b0c17b 100644 --- a/ios/MullvadSettings/TunnelSettingsUpdate.swift +++ b/ios/MullvadSettings/TunnelSettingsUpdate.swift @@ -14,6 +14,7 @@ public enum TunnelSettingsUpdate { case obfuscation(WireGuardObfuscationSettings) case relayConstraints(RelayConstraints) case quantumResistance(TunnelQuantumResistance) + case multihop(MultihopState) } extension TunnelSettingsUpdate { @@ -27,6 +28,9 @@ extension TunnelSettingsUpdate { settings.relayConstraints = newRelayConstraints case let .quantumResistance(newQuantumResistance): settings.tunnelQuantumResistance = newQuantumResistance + case let .multihop(newMultihopState): + // TODO: update setting with the new state + return } } @@ -36,6 +40,7 @@ extension TunnelSettingsUpdate { case .obfuscation: "obfuscation settings" case .relayConstraints: "relay constraints" case .quantumResistance: "quantum resistance" + case .multihop: "multihop" } } } diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index f0d8c26ec0d8..11be6b83257c 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -86,6 +86,8 @@ public enum AccessibilityIdentifier: String { case cityLocationCell case relayLocationCell case customListLocationCell + case multihopPromptAlertBackButton + case multihopPromptAlertEnableButton // Labels case accountPageDeviceNameLabel @@ -183,6 +185,10 @@ public enum AccessibilityIdentifier: String { case quantumResistanceOff case quantumResistanceOn + // Multihop + case multihopEnableSwitch + case multihopPromptAlert + // Error case unknown } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift index 32d72cb2189b..4b8704383567 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift @@ -120,7 +120,7 @@ final class CustomDNSDataSource: UITableViewDiffableDataSource< private let cellFactory: CustomDNSCellFactory private weak var tableView: UITableView? - weak var delegate: VPNSettingsDataSourceDelegate? + weak var delegate: DnsSettingsDataSourceDelegate? init(tableView: UITableView) { self.tableView = tableView diff --git a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift index 67e6ffce19ab..b72b21b5150e 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift @@ -9,7 +9,7 @@ import MullvadSettings import UIKit -class CustomDNSViewController: UITableViewController, VPNSettingsDataSourceDelegate { +class CustomDNSViewController: UITableViewController { private let interactor: VPNSettingsInteractor private var dataSource: CustomDNSDataSource? private let alertPresenter: AlertPresenter @@ -93,9 +93,9 @@ class CustomDNSViewController: UITableViewController, VPNSettingsDataSourceDeleg alertPresenter.showAlert(presentation: presentation, animated: true) } +} - // MARK: - VPNSettingsDataSourceDelegate - +extension CustomDNSViewController: DnsSettingsDataSourceDelegate { func didChangeViewModel(_ viewModel: VPNSettingsViewModel) { interactor.updateSettings([.dnsSettings(viewModel.asDNSSettings())]) } @@ -133,16 +133,4 @@ class CustomDNSViewController: UITableViewController, VPNSettingsDataSourceDeleg showInfo(with: message) } - - func showDNSSettings() { - // No op. - } - - func showIPOverrides() { - // No op. - } - - func didSelectWireGuardPort(_ port: UInt16?) { - // No op. - } } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift index bd3b42b76a85..d439ddbd98e3 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift @@ -14,6 +14,7 @@ protocol VPNSettingsCellEventHandler { func addCustomPort(_ port: UInt16) func selectCustomPortEntry(_ port: UInt16) -> Bool func selectObfuscationState(_ state: WireGuardObfuscationState) + func switchMultihop(_ state: MultihopState, _ discardHandler: @escaping () -> Void) } final class VPNSettingsCellFactory: CellFactoryProtocol { @@ -206,6 +207,24 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { cell.accessibilityIdentifier = item.accessibilityIdentifier cell.applySubCellStyling() #endif + + case .multihop: + guard let cell = cell as? SettingsSwitchCell else { return } + + cell.titleLabel.text = NSLocalizedString( + "MULTIHOP_LABEL", + tableName: "VPNSettings", + value: "Enable multihop", + comment: "" + ) + cell.accessibilityIdentifier = item.accessibilityIdentifier + cell.setOn(viewModel.multihopState == .on, animated: false) + + cell.action = { [weak self] value in + self?.delegate?.switchMultihop(value ? .on : .off) { + cell.setOn(false, animated: true) + } + } } } } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift index ed4f2727b3d0..e658d68a2bf8 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift @@ -23,6 +23,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardObfuscation case wireGuardObfuscationPort case quantumResistance + case multihop var reusableViewClass: AnyClass { switch self { case .dnsSettings: @@ -39,6 +40,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return SelectableSettingsCell.self case .quantumResistance: return SelectableSettingsCell.self + case .multihop: + return SettingsSwitchCell.self } } } @@ -60,6 +63,10 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< #if DEBUG case quantumResistance #endif + + #if DEBUG + case multiHop + #endif } enum Item: Hashable { @@ -76,6 +83,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case quantumResistanceOn case quantumResistanceOff #endif + case multihop static var wireGuardPorts: [Item] { let defaultPorts = VPNSettingsViewModel.defaultWireGuardPorts.map { @@ -124,6 +132,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case .quantumResistanceOff: return .quantumResistanceOff #endif + case .multihop: + return .multihopEnableSwitch } } @@ -145,6 +155,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case .quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff: return .quantumResistance #endif + case .multihop: + return .multihop } } } @@ -417,6 +429,10 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< snapshot.appendItems([.dnsSettings], toSection: .dnsSettings) snapshot.appendItems([.ipOverrides], toSection: .ipOverrides) + #if DEBUG + snapshot.appendItems([.multihop], toSection: .multiHop) + #endif + applySnapshot(snapshot, animated: animated, completion: completion) } @@ -620,6 +636,21 @@ extension VPNSettingsDataSource: VPNSettingsCellEventHandler { func selectQuantumResistance(_ state: TunnelQuantumResistance) { viewModel.setQuantumResistance(state) } + + func switchMultihop(_ state: MultihopState, _ discardHandler: @escaping () -> Void) { + if viewModel.multihopState == .off { + delegate?.showMultihopConfirmation({ [weak self] in + guard let self else { return } + viewModel.setMultihop(.on) + delegate?.didChangeViewModel(viewModel) + }, { + discardHandler() + }) + } else { + viewModel.setMultihop(.off) + delegate?.didChangeViewModel(viewModel) + } + } } // swiftlint:disable:this file_length diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift index 70ecf368be10..f0cf18bea9a4 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift @@ -7,6 +7,12 @@ // import Foundation +import MullvadSettings + +protocol DnsSettingsDataSourceDelegate: AnyObject { + func didChangeViewModel(_ viewModel: VPNSettingsViewModel) + func showInfo(for: VPNSettingsInfoButtonItem) +} protocol VPNSettingsDataSourceDelegate: AnyObject { func didChangeViewModel(_ viewModel: VPNSettingsViewModel) @@ -14,4 +20,5 @@ protocol VPNSettingsDataSourceDelegate: AnyObject { func showDNSSettings() func showIPOverrides() func didSelectWireGuardPort(_ port: UInt16?) + func showMultihopConfirmation(_ onSave: @escaping () -> Void, _ onDiscard: @escaping () -> Void) } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift index 3d55f6fd0e9e..c290f334b8d9 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift @@ -13,7 +13,7 @@ protocol VPNSettingsViewControllerDelegate: AnyObject { func showIPOverrides() } -class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDelegate { +class VPNSettingsViewController: UITableViewController { private let interactor: VPNSettingsInteractor private var dataSource: VPNSettingsDataSource? private let alertPresenter: AlertPresenter @@ -104,9 +104,9 @@ class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDel } .joined(separator: ", ") } +} - // MARK: - VPNSettingsDataSourceDelegate - +extension VPNSettingsViewController: VPNSettingsDataSourceDelegate { func didChangeViewModel(_ viewModel: VPNSettingsViewModel) { interactor.updateSettings( [ @@ -115,6 +115,7 @@ class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDel port: viewModel.obfuscationPort )), .quantumResistance(viewModel.quantumResistance), + .multihop(viewModel.multihopState), ] ) } @@ -175,7 +176,6 @@ class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDel """, comment: "" ) - default: assertionFailure("No matching InfoButtonItem") } @@ -195,4 +195,48 @@ class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDel func didSelectWireGuardPort(_ port: UInt16?) { interactor.setPort(port) } + + func showMultihopConfirmation(_ onSave: @escaping () -> Void, _ onDiscard: @escaping () -> Void) { + let presentation = AlertPresentation( + id: "multihop-prompt-alert", + accessibilityIdentifier: .multihopPromptAlert, + icon: .info, + message: NSLocalizedString( + "MULTIHOP_PROMPT_ALERT_TEXT", + tableName: "Multihop", + value: "This setting increases latency. Use only if needed.", + comment: "" + ), + buttons: [ + AlertAction( + title: NSLocalizedString( + "MULTIHOP_PROMPT_ALERT_ENABLE_BUTTON", + tableName: "Multihop", + value: "Enable anyway", + comment: "" + ), + style: .destructive, + accessibilityId: .multihopPromptAlertEnableButton, + handler: { + onSave() + } + ), + AlertAction( + title: NSLocalizedString( + "MULTIHOP_PROMPT_ALERT_BACK_BUTTON", + tableName: "Multihop", + value: "Back", + comment: "" + ), + style: .default, + accessibilityId: .multihopPromptAlertBackButton, + handler: { + onDiscard() + } + ), + ] + ) + + alertPresenter.showAlert(presentation: presentation, animated: true) + } } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift index 36c9e080d329..df5d5788fe2d 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift @@ -99,6 +99,7 @@ struct VPNSettingsViewModel: Equatable { private(set) var obfuscationPort: WireGuardObfuscationPort private(set) var quantumResistance: TunnelQuantumResistance + private(set) var multihopState: MultihopState static let defaultWireGuardPorts: [UInt16] = [51820, 53] @@ -154,6 +155,10 @@ struct VPNSettingsViewModel: Equatable { quantumResistance = newState } + mutating func setMultihop(_ newState: MultihopState) { + multihopState = newState + } + /// Precondition for enabling Custom DNS. var customDNSPrecondition: CustomDNSPrecondition { if blockAdvertising || blockTracking || blockMalware || @@ -201,6 +206,7 @@ struct VPNSettingsViewModel: Equatable { obfuscationPort = tunnelSettings.wireGuardObfuscation.port quantumResistance = tunnelSettings.tunnelQuantumResistance + multihopState = tunnelSettings.tunnelMultihopState } /// Produce merged view model keeping entry `identifier` for matching DNS entries.