From cbce176ba180f9ce42a89460946311e43cf0ffbb Mon Sep 17 00:00:00 2001 From: mojganii Date: Wed, 28 Aug 2024 11:56:03 +0200 Subject: [PATCH] Add UI for DAITA in VPN settings --- ios/MullvadVPN.xcodeproj/project.pbxproj | 4 + .../Classes/AccessbilityIdentifier.swift | 9 +- .../Login/LoginContentView.swift | 3 + .../Login/LoginViewController.swift | 11 - .../VPNSettings/VPNSettingsCellFactory.swift | 27 ++- .../VPNSettings/VPNSettingsDataSource.swift | 51 +++-- .../VPNSettingsDataSourceDelegate.swift | 1 + .../VPNSettingsInfoButtonItem.swift | 5 + .../VPNSettingsViewController.swift | 193 ++++++++++++------ .../VPNSettings/VPNSettingsViewModel.swift | 6 + ios/MullvadVPNUITests/AccountTests.swift | 4 +- .../Pages/DaitaPromptAlert.swift | 28 +++ .../Pages/VPNSettingsPage.swift | 36 +++- .../SettingsMigrationTests.swift | 56 ++--- 14 files changed, 314 insertions(+), 120 deletions(-) create mode 100644 ios/MullvadVPNUITests/Pages/DaitaPromptAlert.swift diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 6419df3898f6..242db1286303 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -909,6 +909,7 @@ F08B6B7D2C528C6300D0A121 /* PostQuantumKeyExchangingPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05919762C453FAF00C301F3 /* PostQuantumKeyExchangingPipeline.swift */; }; F08B6B7E2C528C6300D0A121 /* MultiHopPostQuantumKeyExchanging.swift in Sources */ = {isa = PBXBuildFile; fileRef = F059197C2C454C9200C301F3 /* MultiHopPostQuantumKeyExchanging.swift */; }; F08B6B822C52931600D0A121 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = F08B6B812C52931600D0A121 /* WireGuardKitTypes */; }; + F09084682C6E88ED001CD36E /* DaitaPromptAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09084672C6E88ED001CD36E /* DaitaPromptAlert.swift */; }; F09A297B2A9F8A9B00EA3B6F /* LogoutDialogueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A29782A9F8A9B00EA3B6F /* LogoutDialogueView.swift */; }; F09A297C2A9F8A9B00EA3B6F /* VoucherTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A29792A9F8A9B00EA3B6F /* VoucherTextField.swift */; }; F09A297D2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A297A2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift */; }; @@ -2106,6 +2107,7 @@ F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = ""; }; F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherOperation.swift; sourceTree = ""; }; F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = ""; }; + F09084672C6E88ED001CD36E /* DaitaPromptAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaitaPromptAlert.swift; sourceTree = ""; }; F09A29782A9F8A9B00EA3B6F /* LogoutDialogueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogoutDialogueView.swift; sourceTree = ""; }; F09A29792A9F8A9B00EA3B6F /* VoucherTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoucherTextField.swift; sourceTree = ""; }; F09A297A2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherContentView.swift; sourceTree = ""; }; @@ -3932,6 +3934,7 @@ 85021CAD2BDBC4290098B400 /* AppLogsPage.swift */, 8587A05C2B84D43100152938 /* ChangeLogAlert.swift */, A9BFB0002BD00B7F00F2BCA1 /* CustomListPage.swift */, + F09084672C6E88ED001CD36E /* DaitaPromptAlert.swift */, 85A42B872BB44D31007BABF7 /* DeviceManagementPage.swift */, 852A26452BA9C9CB006EB9C8 /* DNSSettingsPage.swift */, 8585CBE22BC684180015B6A4 /* EditAccessMethodPage.swift */, @@ -6067,6 +6070,7 @@ 85EC620C2B838D10005AFFB5 /* MullvadAPIWrapper.swift in Sources */, A9DF789D2B7D1E8B0094E4AD /* LoggedInWithTimeUITestCase.swift in Sources */, 85D2B0B12B6BD32400DF9DA7 /* BaseUITestCase.swift in Sources */, + F09084682C6E88ED001CD36E /* DaitaPromptAlert.swift in Sources */, 8529693C2B4F0257007EAD4C /* Alert.swift in Sources */, 8542F7532BCFBD050035C042 /* SelectLocationFilterPage.swift in Sources */, 850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index d434d39b7151..c673fdfe5797 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -89,8 +89,8 @@ public enum AccessibilityIdentifier: String { case cityLocationCell case relayLocationCell case customListLocationCell - case multihopConfirmAlertBackButton - case multihopConfirmAlertEnableButton + case daitaConfirmAlertBackButton + case daitaConfirmAlertEnableButton // Labels case accountPageDeviceNameLabel @@ -190,6 +190,10 @@ public enum AccessibilityIdentifier: String { case dnsServer case dnsServerInfo + // DAITA + case daitaSwitch + case daitaPromptAlert + // Quantum resistance case quantumResistanceAutomatic case quantumResistanceOff @@ -197,7 +201,6 @@ public enum AccessibilityIdentifier: String { // Multihop case multihopSwitch - case multihopPromptAlert // Error case unknown diff --git a/ios/MullvadVPN/View controllers/Login/LoginContentView.swift b/ios/MullvadVPN/View controllers/Login/LoginContentView.swift index 5e1bfec865a6..209b4655280f 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginContentView.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginContentView.swift @@ -43,6 +43,7 @@ class LoginContentView: UIView { let statusActivityView: StatusActivityView = { let statusActivityView = StatusActivityView(state: .hidden) statusActivityView.translatesAutoresizingMaskIntoConstraints = false + statusActivityView.clipsToBounds = true return statusActivityView }() @@ -151,6 +152,8 @@ class LoginContentView: UIView { createAccountButton.pinEdges(.all().excluding(.top), to: footerContainer.layoutMarginsGuide) statusActivityView.centerXAnchor.constraint(equalTo: contentContainer.centerXAnchor) + statusActivityView.widthAnchor.constraint(equalToConstant: 60.0) + statusActivityView.heightAnchor.constraint(equalTo: statusActivityView.widthAnchor, multiplier: 1.0) formContainer.topAnchor.constraint(equalTo: statusActivityView.bottomAnchor, constant: 30) formContainer.centerYAnchor.constraint(equalTo: contentContainer.centerYAnchor, constant: -20) diff --git a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift index bf6c2d140105..af9698f89c06 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift @@ -256,17 +256,6 @@ class LoginViewController: UIViewController, RootContainment { private func updateStatusIcon() { contentView.statusActivityView.state = loginState.statusActivityState - - switch loginState { - case .authenticating: - contentView.statusActivityView.accessibilityIdentifier = .loginStatusIconAuthenticating - case .failure: - contentView.statusActivityView.accessibilityIdentifier = .loginStatusIconFailure - case .success: - contentView.statusActivityView.accessibilityIdentifier = .loginStatusIconSuccess - default: - break - } } private func beginLogin(_ action: LoginAction) { diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift index 0817c8ad0610..42f1c29afcc9 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift @@ -15,6 +15,7 @@ protocol VPNSettingsCellEventHandler { func selectCustomPortEntry(_ port: UInt16) -> Bool func selectObfuscationState(_ state: WireGuardObfuscationState) func switchMultihop(_ state: MultihopState) + func switchDaitaState(_ settings: DAITASettings) } final class VPNSettingsCellFactory: CellFactoryProtocol { @@ -170,7 +171,6 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { ) cell.accessibilityIdentifier = "\(item.accessibilityIdentifier.rawValue)\(portString)" cell.applySubCellStyling() - case .quantumResistanceAutomatic: guard let cell = cell as? SelectableSettingsCell else { return } @@ -206,13 +206,34 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { cell.accessibilityIdentifier = item.accessibilityIdentifier cell.applySubCellStyling() - case .multihop: + case .daitaSwitch: + guard let cell = cell as? SettingsSwitchCell else { return } + + cell.titleLabel.text = NSLocalizedString( + "DAITA_LABEL", + tableName: "VPNSettings", + value: "DAITA", + comment: "" + ) + cell.accessibilityIdentifier = item.accessibilityIdentifier + cell.setOn(viewModel.daitaSettings.state.isEnabled, animated: false) + + cell.infoButtonHandler = { [weak self] in + self?.delegate?.showInfo(for: .daita) + } + + cell.action = { [weak self] isEnabled in + let state: DAITAState = isEnabled ? .on : .off + self?.delegate?.switchDaitaState(DAITASettings(state: state)) + } + + case .multihopSwitch: guard let cell = cell as? SettingsSwitchCell else { return } cell.titleLabel.text = NSLocalizedString( "MULTIHOP_LABEL", tableName: "VPNSettings", - value: "Enable multihop", + value: "Multihop", comment: "" ) cell.accessibilityIdentifier = item.accessibilityIdentifier diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift index 9a44019dddef..228ff6bfcbd5 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift @@ -24,6 +24,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardObfuscationPort case quantumResistance case multihop + case daita var reusableViewClass: AnyClass { switch self { case .dnsSettings: @@ -42,12 +43,14 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return SelectableSettingsCell.self case .multihop: return SettingsSwitchCell.self + case .daita: + return SettingsSwitchCell.self } } } private enum HeaderFooterReuseIdentifiers: String, CaseIterable { - case wireGuardPortHeader + case settingsHeaderView var reusableViewClass: AnyClass { return SettingsHeaderView.self @@ -61,7 +64,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardObfuscation case wireGuardObfuscationPort case quantumResistance - case multiHop + case privacyAndSecurity } enum Item: Hashable { @@ -76,7 +79,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case quantumResistanceAutomatic case quantumResistanceOn case quantumResistanceOff - case multihop + case multihopSwitch + case daitaSwitch static var wireGuardPorts: [Item] { let defaultPorts = VPNSettingsViewModel.defaultWireGuardPorts.map { @@ -121,7 +125,9 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return .quantumResistanceOn case .quantumResistanceOff: return .quantumResistanceOff - case .multihop: + case .daitaSwitch: + return .daitaSwitch + case .multihopSwitch: return .multihopSwitch } } @@ -142,8 +148,10 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return .wireGuardObfuscationPort case .quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff: return .quantumResistance - case .multihop: + case .multihopSwitch: return .multihop + case .daitaSwitch: + return .daita } } } @@ -319,8 +327,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< guard let view = tableView .dequeueReusableHeaderFooterView( - withIdentifier: HeaderFooterReuseIdentifiers.wireGuardPortHeader - .rawValue + withIdentifier: HeaderFooterReuseIdentifiers.settingsHeaderView.rawValue ) as? SettingsHeaderView else { return nil } switch sectionIdentifier { @@ -353,8 +360,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< let sectionIdentifier = snapshot().sectionIdentifiers[section] switch sectionIdentifier { - case .dnsSettings, .ipOverrides, .multiHop: - return 0 + case .dnsSettings, .ipOverrides, .privacyAndSecurity: + return .leastNonzeroMagnitude default: return tableView.estimatedRowHeight } @@ -375,7 +382,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< let sectionIdentifier = snapshot().sectionIdentifiers[indexPath.section] return switch sectionIdentifier { - case .multiHop: false + case .privacyAndSecurity: false default: true } } @@ -392,7 +399,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< tableView?.register( SettingsHeaderView.self, - forHeaderFooterViewReuseIdentifier: HeaderFooterReuseIdentifiers.wireGuardPortHeader.rawValue + forHeaderFooterViewReuseIdentifier: HeaderFooterReuseIdentifiers.settingsHeaderView.rawValue ) } @@ -412,7 +419,11 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< snapshot.appendItems([.dnsSettings], toSection: .dnsSettings) snapshot.appendItems([.ipOverrides], toSection: .ipOverrides) - snapshot.appendItems([.multihop], toSection: .multiHop) + #if DEBUG + snapshot.appendItems([.daitaSwitch, .multihopSwitch], toSection: .privacyAndSecurity) + #else + snapshot.appendItems([.multihopSwitch], toSection: .privacyAndSecurity) + #endif applySnapshot(snapshot, animated: animated, completion: completion) } @@ -620,6 +631,22 @@ extension VPNSettingsDataSource: VPNSettingsCellEventHandler { viewModel.setMultihop(state) delegate?.didChangeViewModel(viewModel) } + + func switchDaitaState(_ settings: DAITASettings) { + if settings.state.isEnabled { + delegate?.showPrompt(for: .daita) { [weak self] in + guard let self else { return } + viewModel.setDAITASettings(settings) + delegate?.didChangeViewModel(viewModel) + } onDiscard: { [weak self] in + guard let self else { return } + tableView?.reloadData() + } + } else { + viewModel.setDAITASettings(settings) + 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 48e952c7a678..91e2108fc20b 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift @@ -20,4 +20,5 @@ protocol VPNSettingsDataSourceDelegate: AnyObject { func showDNSSettings() func showIPOverrides() func didSelectWireGuardPort(_ port: UInt16?) + func showPrompt(for: VPNSettingsPromptAlertItem, onSave: @escaping () -> Void, onDiscard: @escaping () -> Void) } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift index a6fb585b3376..978ccdce8696 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift @@ -14,4 +14,9 @@ enum VPNSettingsInfoButtonItem { case wireGuardObfuscationPort case quantumResistance case multihop + case daita +} + +enum VPNSettingsPromptAlertItem { + case daita } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift index a0833d7c4307..8558924c1b94 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift @@ -115,83 +115,93 @@ extension VPNSettingsViewController: VPNSettingsDataSourceDelegate { )), .quantumResistance(viewModel.quantumResistance), .multihop(viewModel.multihopState), + .daita(viewModel.daitaSettings), ] ) } // swiftlint:disable:next function_body_length - func showInfo(for item: VPNSettingsInfoButtonItem) { - var message = "" - - switch item { - case .wireGuardPorts: - let portsString = humanReadablePortRepresentation( - interactor.cachedRelays?.relays.wireguard.portRanges ?? [] - ) - - message = String( - format: NSLocalizedString( - "VPN_SETTINGS_WIRE_GUARD_PORTS_GENERAL", - tableName: "WireGuardPorts", - value: """ - The automatic setting will randomly choose from the valid port ranges shown below. - The custom port can be any value inside the valid ranges: - %@ - """, - comment: "" - ), - portsString - ) + func showInfo(for item: VPNSettingsInfoButtonItem) { switch item { + case .wireGuardPorts: + let portsString = humanReadablePortRepresentation( + interactor.cachedRelays?.relays.wireguard.portRanges ?? [] + ) - case .wireGuardObfuscation: - message = NSLocalizedString( - "VPN_SETTINGS_WIRE_GUARD_OBFUSCATION_GENERAL", - tableName: "WireGuardObfuscation", + showInfo(with: String( + format: NSLocalizedString( + "VPN_SETTINGS_WIRE_GUARD_PORTS_GENERAL", + tableName: "WireGuardPorts", value: """ - Obfuscation hides the WireGuard traffic inside another protocol. \ - It can be used to help circumvent censorship and other types of filtering, \ - where a plain WireGuard connect would be blocked. + The automatic setting will randomly choose from the valid port ranges shown below. + The custom port can be any value inside the valid ranges: + %@ """, comment: "" - ) - - case .wireGuardObfuscationPort: - message = NSLocalizedString( - "VPN_SETTINGS_WIRE_GUARD_OBFUSCATION_PORT_GENERAL", - tableName: "WireGuardObfuscation", - value: "Which TCP port the UDP-over-TCP obfuscation protocol should connect to on the VPN server.", - comment: "" - ) + ), + portsString + )) - case .quantumResistance: - message = NSLocalizedString( - "VPN_SETTINGS_QUANTUM_RESISTANCE_GENERAL", - tableName: "QuantumResistance", - value: """ - This feature makes the WireGuard tunnel resistant to potential attacks from quantum computers. - It does this by performing an extra key exchange using a quantum safe algorithm and mixing \ - the result into WireGuard’s regular encryption. - This extra step uses approximately 500 kiB of traffic every time a new tunnel is established. - """, - comment: "" - ) + case .wireGuardObfuscation: + showInfo(with: NSLocalizedString( + "VPN_SETTINGS_WIRE_GUARD_OBFUSCATION_GENERAL", + tableName: "WireGuardObfuscation", + value: """ + Obfuscation hides the WireGuard traffic inside another protocol. \ + It can be used to help circumvent censorship and other types of filtering, \ + where a plain WireGuard connect would be blocked. + """, + comment: "" + )) - case .multihop: - message = NSLocalizedString( - "MULTIHOP_INFORMATION_TEXT", - tableName: "Multihop", - value: """ - Multihop routes your traffic into one WireGuard server and out another, making it harder to trace. - This results in increased latency but increases anonymity online. - """, + case .wireGuardObfuscationPort: + showInfo(with: NSLocalizedString( + "VPN_SETTINGS_WIRE_GUARD_OBFUSCATION_PORT_GENERAL", + tableName: "WireGuardObfuscation", + value: "Which TCP port the UDP-over-TCP obfuscation protocol should connect to on the VPN server.", + comment: "" + )) - comment: "" - ) - default: - assertionFailure("No matching InfoButtonItem") - } + case .quantumResistance: + showInfo(with: NSLocalizedString( + "VPN_SETTINGS_QUANTUM_RESISTANCE_GENERAL", + tableName: "QuantumResistance", + value: """ + This feature makes the WireGuard tunnel resistant to potential attacks from quantum computers. + It does this by performing an extra key exchange using a quantum safe algorithm and mixing \ + the result into WireGuard’s regular encryption. + This extra step uses approximately 500 kiB of traffic every time a new tunnel is established. + """, + comment: "" + )) - showInfo(with: message) + case .multihop: + showInfo(with: NSLocalizedString( + "MULTIHOP_INFORMATION_TEXT", + tableName: "Multihop", + value: """ + Multihop routes your traffic into one WireGuard server and out another, making it harder to trace. + This results in increased latency but increases anonymity online. + """, + comment: "" + )) + case .daita: + showInfo(with: NSLocalizedString( + "DAITA_INFORMATION_TEXT", + tableName: "DAITA", + value: """ + DAITA (Defence against AI-guided Traffic Analysis) hides patterns in your encrypted VPN traffic. \ + If anyone is monitoring your connection, this makes it significantly harder for them to identify\ + what websites you are visiting. + It does this by carefully adding network noise and making all network packets the same size. + Attention: Since this increases your total network traffic,\ + be cautious if you have a limited data plan.\ + It can also negatively impact your network speed. + """, + comment: "" + )) + default: + assertionFailure("No matching InfoButtonItem") + } } func showDNSSettings() { @@ -206,4 +216,59 @@ extension VPNSettingsViewController: VPNSettingsDataSourceDelegate { func didSelectWireGuardPort(_ port: UInt16?) { interactor.setPort(port) } + + func showPrompt( + for item: VPNSettingsPromptAlertItem, + onSave: @escaping () -> Void, + onDiscard: @escaping () -> Void + ) { + switch item { + case .daita: + let presentation = AlertPresentation( + id: "vpn-settings-content-blockers-alert", + accessibilityIdentifier: .daitaPromptAlert, + icon: .info, + message: NSLocalizedString( + "DAITA_INFORMATION_TEXT", + tableName: "DAITA", + value: """ + This feature isn't available on all servers. \ + You might need to change location after enabling. + Attention: Since this increases your total network traffic,\ + be cautious if you have a limited data plan. It can also \ + negatively impact your network speed. Please consider \ + this if you want to enable DAITA. + """, + comment: "" + ), + buttons: [ + AlertAction( + title: NSLocalizedString( + "VPN_SETTINGS_VPN_DAITA_OK_ACTION", + tableName: "DAITA", + value: "Enable anyway", + comment: "" + ), + style: .default, + accessibilityId: .daitaConfirmAlertEnableButton, + handler: { + onSave() + } + ), + AlertAction( + title: NSLocalizedString( + "VPN_SETTINGS_VPN_DAITA_CANCEL_ACTION", + tableName: "DAITA", + value: "Back", + comment: "" + ), + style: .default, 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 85993c3dee69..94fde408b6a9 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift @@ -100,6 +100,7 @@ struct VPNSettingsViewModel: Equatable { private(set) var quantumResistance: TunnelQuantumResistance private(set) var multihopState: MultihopState + private(set) var daitaSettings: DAITASettings static let defaultWireGuardPorts: [UInt16] = [51820, 53] @@ -159,6 +160,10 @@ struct VPNSettingsViewModel: Equatable { multihopState = newState } + mutating func setDAITASettings(_ newSettings: DAITASettings) { + daitaSettings = newSettings + } + /// Precondition for enabling Custom DNS. var customDNSPrecondition: CustomDNSPrecondition { if blockAdvertising || blockTracking || blockMalware || @@ -207,6 +212,7 @@ struct VPNSettingsViewModel: Equatable { quantumResistance = tunnelSettings.tunnelQuantumResistance multihopState = tunnelSettings.tunnelMultihopState + daitaSettings = tunnelSettings.daita } /// Produce merged view model keeping entry `identifier` for matching DNS entries. diff --git a/ios/MullvadVPNUITests/AccountTests.swift b/ios/MullvadVPNUITests/AccountTests.swift index 617b2b3ce131..8b6814b4d0ea 100644 --- a/ios/MullvadVPNUITests/AccountTests.swift +++ b/ios/MullvadVPNUITests/AccountTests.swift @@ -72,12 +72,12 @@ class AccountTests: LoggedOutUITestCase { var retryCount = 0 let maxRetryCount = 3 - LoginPage(app) + let loginPage = LoginPage(app) .tapAccountNumberTextField() .enterText(hasTimeAccountNumber) repeat { - successIconShown = LoginPage(app) + successIconShown = loginPage .tapAccountNumberSubmitButton() .getSuccessIconShown() diff --git a/ios/MullvadVPNUITests/Pages/DaitaPromptAlert.swift b/ios/MullvadVPNUITests/Pages/DaitaPromptAlert.swift new file mode 100644 index 000000000000..beea7f6b026a --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/DaitaPromptAlert.swift @@ -0,0 +1,28 @@ +// +// DaitaPromptAlert.swift +// MullvadVPNUITests +// +// Created by Mojgan on 2024-08-15. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// +import Foundation +import XCTest + +class DaitaPromptAlert: Page { + @discardableResult override init(_ app: XCUIApplication) { + super.init(app) + + self.pageElement = app.otherElements[.daitaPromptAlert] + waitForPageToBeShown() + } + + @discardableResult func tapEnableAnyway() -> Self { + app.buttons[AccessibilityIdentifier.daitaConfirmAlertEnableButton].tap() + return self + } + + @discardableResult func tapBack() -> Self { + app.buttons[AccessibilityIdentifier.daitaConfirmAlertBackButton].tap() + return self + } +} diff --git a/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift b/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift index 5746ce7ce00e..cc4d6e74d701 100644 --- a/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift +++ b/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift @@ -126,6 +126,21 @@ class VPNSettingsPage: Page { return self } + @discardableResult func tapDaitaSwitch() -> Self { + app.cells[AccessibilityIdentifier.daitaSwitch] + .switches[AccessibilityIdentifier.customSwitch] + .tap() + let promptIsShown = app + .otherElements[AccessibilityIdentifier.daitaPromptAlert.rawValue] + .waitForExistence(timeout: 1.0) + + if promptIsShown { + DaitaPromptAlert(app) + .tapEnableAnyway() + } + return self + } + @discardableResult func verifyCustomWireGuardPortSelected(portNumber: String) -> Self { let cell = app.cells[AccessibilityIdentifier.wireGuardCustomPort] XCTAssertTrue(cell.isSelected) @@ -158,8 +173,27 @@ class VPNSettingsPage: Page { return self } + @discardableResult func verifyQuantumResistantTunnelOnSelected() -> Self { + let cell = app.cells[AccessibilityIdentifier.quantumResistanceOn] + XCTAssertTrue(cell.isSelected) + return self + } + @discardableResult func verifyMultihopSwitchOn() -> Self { - let switchElement = app.cells[AccessibilityIdentifier.multihopSwitch] + let switchElement = app.cells[.multihopSwitch] + .switches[AccessibilityIdentifier.customSwitch] + + guard let switchValue = switchElement.value as? String else { + XCTFail("Failed to read switch state") + return self + } + + XCTAssertEqual(switchValue, "1") + return self + } + + @discardableResult func verifyDaitaSwitchOn() -> Self { + let switchElement = app.cells[.daitaSwitch] .switches[AccessibilityIdentifier.customSwitch] guard let switchValue = switchElement.value as? String else { diff --git a/ios/MullvadVPNUITests/SettingsMigrationTests.swift b/ios/MullvadVPNUITests/SettingsMigrationTests.swift index f9898f703b4e..85ab71bafa34 100644 --- a/ios/MullvadVPNUITests/SettingsMigrationTests.swift +++ b/ios/MullvadVPNUITests/SettingsMigrationTests.swift @@ -9,6 +9,15 @@ import Foundation import XCTest +/* + Settings migration is an exception, it uses four different test plans and a separate workflow + `ios-end-to-end-tests-settings-migration.yml` which executes the test plans in order, + do not reinstall the app in between runs but upgrades the app after changing settings: + * `MullvadVPNUITestsChangeDNSSettings` - Change settings for using custom DNS + * `MullvadVPNUITestsVerifyDNSSettingsChanged` - Verify custom DNS settings still changed + * `MullvadVPNUITestsChangeSettings` - Change all settings except custom DNS setting + * `MullvadVPNUITestsVerifySettingsChanged` - Verify all settings except custom DNS setting still changed + */ class SettingsMigrationTests: BaseUITestCase { let customDNSServerIPAddress = "123.123.123.123" let wireGuardPort = "4001" @@ -57,7 +66,22 @@ class SettingsMigrationTests: BaseUITestCase { .tapDoneButton() } - func testChangeSettings() { + func testVerifyCustomDNSSettingsStillChanged() { + HeaderBar(app) + .tapSettingsButton() + + SettingsPage(app) + .tapVPNSettingsCell() + + VPNSettingsPage(app) + .tapDNSSettingsCell() + + DNSSettingsPage(app) + .verifyUseCustomDNSSwitchOn() + .verifyCustomDNSIPAddress(customDNSServerIPAddress) + } + + func testChangeVPNSettings() { let hasTimeAccountNumber = getAccountWithTime() addTeardownBlock { @@ -81,8 +105,8 @@ class SettingsMigrationTests: BaseUITestCase { .tapBlockAdsSwitch() .tapBlockTrackerSwitch() .tapBlockMalwareSwitch() - .tapBlockAdultContentSwitch() .tapBlockGamblingSwitch() + .tapBlockAdultContentSwitch() .tapBlockSocialMediaSwitch() .tapBackButton() @@ -91,31 +115,16 @@ class SettingsMigrationTests: BaseUITestCase { .tapCustomWireGuardPortTextField() .enterText(wireGuardPort) .dismissKeyboard() - .tapWireGuardPortsExpandButton() .tapWireGuardObfuscationExpandButton() .tapWireGuardObfuscationOnCell() - .tapWireGuardObfuscationExpandButton() .tapUDPOverTCPPortExpandButton() .tapUDPOverTCPPort80Cell() - .tapUDPOverTCPPortExpandButton() + .tapQuantumResistantTunnelExpandButton() + .tapQuantumResistantTunnelOnCell() + .tapDaitaSwitch() .tapMultihopSwitch() } - func testVerifyCustomDNSSettingsStillChanged() { - HeaderBar(app) - .tapSettingsButton() - - SettingsPage(app) - .tapVPNSettingsCell() - - VPNSettingsPage(app) - .tapDNSSettingsCell() - - DNSSettingsPage(app) - .verifyUseCustomDNSSwitchOn() - .verifyCustomDNSIPAddress(customDNSServerIPAddress) - } - func testVerifySettingsStillChanged() { HeaderBar(app) .tapSettingsButton() @@ -139,14 +148,13 @@ class SettingsMigrationTests: BaseUITestCase { VPNSettingsPage(app) .tapWireGuardPortsExpandButton() .verifyCustomWireGuardPortSelected(portNumber: wireGuardPort) - .tapWireGuardPortsExpandButton() .tapWireGuardObfuscationExpandButton() - .tapWireGuardObfuscationOnCell() .verifyWireGuardObfuscationOnSelected() - .tapWireGuardObfuscationExpandButton() .tapUDPOverTCPPortExpandButton() .verifyUDPOverTCPPort80Selected() + .tapQuantumResistantTunnelExpandButton() + .verifyQuantumResistantTunnelOnSelected() + .verifyDaitaSwitchOn() .verifyMultihopSwitchOn() - .tapBackButton() } }