diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 2bd37ffa6cc1..a050bda139b2 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -466,6 +466,8 @@ 7A27E3C92CAE85710088BCFF /* SettingsInfoButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3C82CAE85660088BCFF /* SettingsInfoButtonItem.swift */; }; 7A27E3CB2CAE861D0088BCFF /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3CA2CAE86170088BCFF /* SettingsViewModel.swift */; }; 7A27E3CD2CB814EF0088BCFF /* DAITAInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3CC2CB814EA0088BCFF /* DAITAInfoView.swift */; }; + 7A27E3CF2CBD4A8C0088BCFF /* SelectableSettingsDetailsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3CE2CBD4A830088BCFF /* SelectableSettingsDetailsCell.swift */; }; + 7A27E3D12CC299F90088BCFF /* VPNSettingsDetailsButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3D02CC299E60088BCFF /* VPNSettingsDetailsButtonItem.swift */; }; 7A28826A2BA8336600FD9F20 /* VPNSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2882692BA8336600FD9F20 /* VPNSettingsCoordinator.swift */; }; 7A2960F62A963F7500389B82 /* AlertCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */; }; 7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */; }; @@ -1803,6 +1805,8 @@ 7A27E3C82CAE85660088BCFF /* SettingsInfoButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInfoButtonItem.swift; sourceTree = ""; }; 7A27E3CA2CAE86170088BCFF /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 7A27E3CC2CB814EA0088BCFF /* DAITAInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITAInfoView.swift; sourceTree = ""; }; + 7A27E3CE2CBD4A830088BCFF /* SelectableSettingsDetailsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsDetailsCell.swift; sourceTree = ""; }; + 7A27E3D02CC299E60088BCFF /* VPNSettingsDetailsButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsDetailsButtonItem.swift; sourceTree = ""; }; 7A2882692BA8336600FD9F20 /* VPNSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsCoordinator.swift; sourceTree = ""; }; 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCoordinator.swift; sourceTree = ""; }; 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresentation.swift; sourceTree = ""; }; @@ -2803,6 +2807,7 @@ children = ( 7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */, 7A1A264A2A29D65E00B978AA /* SelectableSettingsCell.swift */, + 7A27E3CE2CBD4A830088BCFF /* SelectableSettingsDetailsCell.swift */, 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */, 582BB1AE229566420055B6EF /* SettingsCell.swift */, 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */, @@ -2845,6 +2850,7 @@ 5864AF0229C7879B005B0CD9 /* VPNSettingsCellFactory.swift */, 584D26C3270C855A004EA533 /* VPNSettingsDataSource.swift */, 587EB6732714520600123C75 /* VPNSettingsDataSourceDelegate.swift */, + 7A27E3D02CC299E60088BCFF /* VPNSettingsDetailsButtonItem.swift */, 7A6F2FAE2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift */, 5871167E2910035700D41AAC /* VPNSettingsInteractor.swift */, 58ACF6482655365700ACE4B7 /* VPNSettingsViewController.swift */, @@ -5658,6 +5664,7 @@ A91614D62B10B26B00F416EB /* TunnelControlViewModel.swift in Sources */, 7A5869972B32EA4500640D27 /* AppButton.swift in Sources */, 586C0D8F2B03D88100E7CDD7 /* ProxyProtocolConfigurationItemIdentifier.swift in Sources */, + 7A27E3CF2CBD4A8C0088BCFF /* SelectableSettingsDetailsCell.swift in Sources */, 7A27E3CB2CAE861D0088BCFF /* SettingsViewModel.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, 7A9F29392CABFAFC005F2089 /* InfoHeaderView.swift in Sources */, @@ -5854,6 +5861,7 @@ F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */, 58FF9FE42B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift in Sources */, 5878A27329091D6D0096FC88 /* TunnelBlockObserver.swift in Sources */, + 7A27E3D12CC299F90088BCFF /* VPNSettingsDetailsButtonItem.swift in Sources */, A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */, 58CEB2E92AFBBA4A00E6E088 /* AddAccessMethodCoordinator.swift in Sources */, 58DFF7D02B02560400F864E0 /* NSAttributedString+Extensions.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 6cfbce70a131..358dd040d2bd 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -174,7 +174,8 @@ public enum AccessibilityIdentifier: String { case wireGuardObfuscationAutomatic case wireGuardObfuscationPort case wireGuardObfuscationOff - case wireGuardObfuscationOn + case wireGuardObfuscationUDPTCP + case wireGuardObfuscationShadowsocks case wireGuardPort // Custom DNS diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift index 9e4997da1ea4..fd9f24407d81 100644 --- a/ios/MullvadVPN/UI appearance/UIMetrics.swift +++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift @@ -93,6 +93,8 @@ enum UIMetrics { static let customListsCellHeight: CGFloat = 44 static let apiAccessSwitchCellTrailingMargin: CGFloat = apiAccessInsetLayoutMargins.trailing - 4 static let apiAccessPickerListContentInsetTop: CGFloat = 16 + static let buttonSeparatorHeight: CGFloat = 22 + static let detailsButtonSize: CGFloat = 60 } enum InAppBannerNotification { diff --git a/ios/MullvadVPN/View controllers/Settings/CheckableSettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/CheckableSettingsCell.swift index a732234473f5..6f7b0cb5ef9d 100644 --- a/ios/MullvadVPN/View controllers/Settings/CheckableSettingsCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/CheckableSettingsCell.swift @@ -14,7 +14,7 @@ class CheckableSettingsCell: SettingsCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - setLeftView(checkboxView, spacing: UIMetrics.SettingsCell.checkableSettingsCellLeftViewSpacing) + setCheckboxView() selectedBackgroundView?.backgroundColor = .clear } @@ -24,8 +24,7 @@ class CheckableSettingsCell: SettingsCell { override func prepareForReuse() { super.prepareForReuse() - - setLeftView(checkboxView, spacing: UIMetrics.SettingsCell.checkableSettingsCellLeftViewSpacing) + setCheckboxView() } override func setSelected(_ selected: Bool, animated: Bool) { @@ -39,4 +38,17 @@ class CheckableSettingsCell: SettingsCell { contentView.layoutMargins.left = 0 } + + private func setCheckboxView() { + setLeadingView { superview in + superview.addConstrainedSubviews([checkboxView]) { + checkboxView.pinEdgesToSuperview(PinnableEdges([ + .leading(0), + .trailing(UIMetrics.SettingsCell.checkableSettingsCellLeftViewSpacing), + .top(0), + .bottom(0), + ])) + } + } + } } diff --git a/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift index 3cc5bb1e76f3..c720dc2066f4 100644 --- a/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift @@ -11,6 +11,7 @@ import UIKit class SelectableSettingsCell: SettingsCell { let tickImageView: UIImageView = { let imageView = UIImageView(image: UIImage(named: "IconTick")) + imageView.contentMode = .center imageView.tintColor = .white imageView.alpha = 0 return imageView @@ -19,7 +20,7 @@ class SelectableSettingsCell: SettingsCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - setLeftView(tickImageView, spacing: UIMetrics.SettingsCell.selectableSettingsCellLeftViewSpacing) + setTickView() selectedBackgroundView?.backgroundColor = UIColor.Cell.Background.selected } @@ -29,8 +30,7 @@ class SelectableSettingsCell: SettingsCell { override func prepareForReuse() { super.prepareForReuse() - - setLeftView(tickImageView, spacing: UIMetrics.SettingsCell.selectableSettingsCellLeftViewSpacing) + setTickView() } override func setSelected(_ selected: Bool, animated: Bool) { @@ -38,4 +38,17 @@ class SelectableSettingsCell: SettingsCell { tickImageView.alpha = selected ? 1 : 0 } + + private func setTickView() { + setLeadingView { superview in + superview.addConstrainedSubviews([tickImageView]) { + tickImageView.pinEdgesToSuperview(PinnableEdges([ + .leading(0), + .trailing(UIMetrics.SettingsCell.selectableSettingsCellLeftViewSpacing), + .top(0), + .bottom(0), + ])) + } + } + } } diff --git a/ios/MullvadVPN/View controllers/Settings/SelectableSettingsDetailsCell.swift b/ios/MullvadVPN/View controllers/Settings/SelectableSettingsDetailsCell.swift new file mode 100644 index 000000000000..3e8ea12e549d --- /dev/null +++ b/ios/MullvadVPN/View controllers/Settings/SelectableSettingsDetailsCell.swift @@ -0,0 +1,69 @@ +// +// SelectableSettingsDetailsCell.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-10-14. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class SelectableSettingsDetailsCell: SelectableSettingsCell { + let viewContainer = UIView() + + var action: (() -> Void)? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) + + let actionButton = IncreasedHitButton(type: .system) + actionButton.setImage(UIImage(systemName: "ellipsis"), for: .normal) + actionButton.tintColor = .white + actionButton.addTarget( + self, + action: #selector(didPressActionButton), + for: .valueChanged + ) + + let separatorView = UIView() + separatorView.backgroundColor = .primaryColor + + viewContainer.addConstrainedSubviews([separatorView, actionButton]) { + separatorView.leadingAnchor.constraint(equalTo: viewContainer.leadingAnchor, constant: 16) + separatorView.centerYAnchor.constraint(equalTo: viewContainer.centerYAnchor) + separatorView.heightAnchor.constraint(equalToConstant: UIMetrics.SettingsCell.buttonSeparatorHeight) + separatorView.widthAnchor.constraint(equalToConstant: 1) + + actionButton.pinEdgesToSuperview(.all().excluding(.leading)) + actionButton.leadingAnchor.constraint(equalTo: separatorView.trailingAnchor) + actionButton.widthAnchor.constraint(equalToConstant: UIMetrics.SettingsCell.detailsButtonSize) + } + + setViewContainer() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + setViewContainer() + } + + private func setViewContainer() { + #if DEBUG + setTrailingView { superview in + superview.addConstrainedSubviews([viewContainer]) { + viewContainer.pinEdgesToSuperview() + } + } + #endif + } + + // MARK: - Actions + + @objc private func didPressActionButton() { + action?() + } +} diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift index c55b5f0aa2f8..dd9f8939e184 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift @@ -31,11 +31,10 @@ enum SettingsDisclosureType { class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { typealias InfoButtonHandler = () -> Void - let contentContainerSubviewMaxCount = 2 - let titleLabel = UILabel() - let detailTitleLabel = UILabel() let disclosureImageView = UIImageView(image: nil) - let contentContainer = UIStackView() + let mainContentContainer = UIView() + let leftContentContainer = UIView() + let rightContentContainer = UIView() var infoButtonHandler: InfoButtonHandler? { didSet { infoButton.isHidden = infoButtonHandler == nil }} @@ -59,8 +58,27 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { } } + let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 17) + label.textColor = UIColor.Cell.titleTextColor + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + return label + }() + + let detailTitleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 13) + label.textColor = UIColor.Cell.detailTextColor + label.setContentHuggingPriority(.defaultLow, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + return label + }() + private var subCellLeadingIndentation: CGFloat = 0 private let buttonWidth: CGFloat = 24 + private let infoButton: UIButton = { let button = UIButton(type: .custom) button.accessibilityIdentifier = .infoButton @@ -83,38 +101,29 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { backgroundColor = .clear contentView.backgroundColor = .clear - infoButton.isHidden = true infoButton.addTarget(self, action: #selector(handleInfoButton(_:)), for: .touchUpInside) subCellLeadingIndentation = contentView.layoutMargins.left + UIMetrics.TableView.cellIndentationWidth - titleLabel.translatesAutoresizingMaskIntoConstraints = false - titleLabel.font = UIFont.systemFont(ofSize: 17) - titleLabel.textColor = UIColor.Cell.titleTextColor - - detailTitleLabel.translatesAutoresizingMaskIntoConstraints = false - detailTitleLabel.font = UIFont.systemFont(ofSize: 13) - detailTitleLabel.textColor = UIColor.Cell.detailTextColor - - titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) - detailTitleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) - - titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - detailTitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + rightContentContainer.setContentHuggingPriority(.required, for: .horizontal) setLayoutMargins() let buttonAreaWidth = UIMetrics.contentLayoutMargins.leading + UIMetrics .contentLayoutMargins.trailing + buttonWidth - let content = UIView() - content.addConstrainedSubviews([titleLabel, infoButton, detailTitleLabel]) { + let infoButtonConstraint = infoButton.trailingAnchor.constraint( + greaterThanOrEqualTo: mainContentContainer.trailingAnchor + ) + infoButtonConstraint.priority = .defaultLow + + mainContentContainer.addConstrainedSubviews([titleLabel, infoButton, detailTitleLabel]) { switch style { case .subtitle: titleLabel.pinEdgesToSuperview(.init([.top(0), .leading(0)])) - detailTitleLabel.pinEdgesToSuperview(.all().excluding(.top)) - detailTitleLabel.topAnchor.constraint(equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1) - infoButton.trailingAnchor.constraint(greaterThanOrEqualTo: content.trailingAnchor) + detailTitleLabel.pinEdgesToSuperview(.all().excluding([.top, .trailing])) + detailTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor) + infoButtonConstraint default: titleLabel.pinEdgesToSuperview(.all().excluding(.trailing)) @@ -122,8 +131,6 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { detailTitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: infoButton.trailingAnchor) } - infoButton.pinEdgesToSuperview(.init([.top(0)])) - infoButton.bottomAnchor.constraint(lessThanOrEqualTo: content.bottomAnchor) infoButton.leadingAnchor.constraint( equalTo: titleLabel.trailingAnchor, constant: -UIMetrics.interButtonSpacing @@ -132,10 +139,14 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { infoButton.widthAnchor.constraint(equalToConstant: buttonAreaWidth) } - contentContainer.addArrangedSubview(content) + contentView.addConstrainedSubviews([leftContentContainer, mainContentContainer, rightContentContainer]) { + mainContentContainer.pinEdgesToSuperviewMargins(.all().excluding([.leading, .trailing])) - contentView.addConstrainedSubviews([contentContainer]) { - contentContainer.pinEdgesToSuperviewMargins() + leftContentContainer.pinEdgesToSuperviewMargins(.all().excluding(.trailing)) + leftContentContainer.trailingAnchor.constraint(equalTo: mainContentContainer.leadingAnchor) + + rightContentContainer.pinEdgesToSuperview(.all().excluding(.leading)) + rightContentContainer.leadingAnchor.constraint(equalTo: mainContentContainer.trailingAnchor) } } @@ -147,7 +158,8 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { super.prepareForReuse() infoButton.isHidden = true - removeLeftView() + removeLeadingView() + removeTrailingView() setLayoutMargins() } @@ -156,20 +168,22 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { backgroundView?.backgroundColor = UIColor.Cell.Background.indentationLevelOne } - func setLeftView(_ view: UIView, spacing: CGFloat) { - removeLeftView() + func setLeadingView(superviewProvider: (UIView) -> Void) { + removeLeadingView() + superviewProvider(leftContentContainer) + } - if contentContainer.arrangedSubviews.count <= 1 { - contentContainer.insertArrangedSubview(view, at: 0) - } + func removeLeadingView() { + leftContentContainer.subviews.forEach { $0.removeFromSuperview() } + } - contentContainer.spacing = spacing + func setTrailingView(superviewProvider: (UIView) -> Void) { + removeTrailingView() + superviewProvider(rightContentContainer) } - func removeLeftView() { - if contentContainer.arrangedSubviews.count >= contentContainerSubviewMaxCount { - contentContainer.arrangedSubviews.first?.removeFromSuperview() - } + func removeTrailingView() { + rightContentContainer.subviews.forEach { $0.removeFromSuperview() } } @objc private func handleInfoButton(_ sender: UIControl) { diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift index 16d6ff24a5cf..6378f0e5b2ef 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift @@ -11,6 +11,7 @@ import UIKit protocol VPNSettingsCellEventHandler { func showInfo(for button: VPNSettingsInfoButtonItem) + func showDetails(for button: VPNSettingsDetailsButtonItem) func addCustomPort(_ port: UInt16) func selectCustomPortEntry(_ port: UInt16) -> Bool func selectObfuscationState(_ state: WireGuardObfuscationState) @@ -135,17 +136,54 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { cell.accessibilityIdentifier = item.accessibilityIdentifier cell.applySubCellStyling() - case .wireGuardObfuscationOn: - guard let cell = cell as? SelectableSettingsCell else { return } + case .wireGuardObfuscationUDPTCP: + guard let cell = cell as? SelectableSettingsDetailsCell else { return } + + cell.titleLabel.text = NSLocalizedString( + "WIRE_GUARD_OBFUSCATION_UDP_TCP_LABEL", + tableName: "VPNSettings", + value: "UDP-over-TCP", + comment: "" + ) + #if DEBUG + cell.detailTitleLabel.text = String(format: NSLocalizedString( + "WIRE_GUARD_OBFUSCATION_UDP_TCP_PORT", + tableName: "VPNSettings", + value: "Port: %d", + comment: "" + ), viewModel.obfuscationPort.portValue) + #endif + cell.accessibilityIdentifier = item.accessibilityIdentifier + cell.applySubCellStyling() + + cell.action = { [weak self] in + self?.delegate?.showDetails(for: .udpTcp) + } + + case .wireGuardObfuscationShadowsocks: + guard let cell = cell as? SelectableSettingsDetailsCell else { return } cell.titleLabel.text = NSLocalizedString( - "WIRE_GUARD_OBFUSCATION_ON_LABEL", + "WIRE_GUARD_OBFUSCATION_SHADOWSOCKS_LABEL", tableName: "VPNSettings", - value: "On (UDP-over-TCP)", + value: "Shadowsocks", comment: "" ) + #if DEBUG + cell.detailTitleLabel.text = String(format: NSLocalizedString( + "WIRE_GUARD_OBFUSCATION_SHADOWSOCKS_PORT", + tableName: "VPNSettings", + value: "Port: %d", + comment: "" + ), viewModel.obfuscationPort.portValue) + #endif cell.accessibilityIdentifier = item.accessibilityIdentifier cell.applySubCellStyling() + + cell.action = { [weak self] in + self?.delegate?.showDetails(for: .shadowsocks) + } + case .wireGuardObfuscationOff: guard let cell = cell as? SelectableSettingsCell else { return } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift index 2aa5f6dd5679..0b03a06a4a8a 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift @@ -21,6 +21,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardPort case wireGuardCustomPort case wireGuardObfuscation + case wireGuardObfuscationOption case wireGuardObfuscationPort case quantumResistance case multihop @@ -35,6 +36,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return SelectableSettingsCell.self case .wireGuardCustomPort: return SettingsInputCell.self + case .wireGuardObfuscationOption: + return SelectableSettingsDetailsCell.self case .wireGuardObfuscation: return SelectableSettingsCell.self case .wireGuardObfuscationPort: @@ -73,7 +76,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardPort(_ port: UInt16?) case wireGuardCustomPort case wireGuardObfuscationAutomatic - case wireGuardObfuscationOn + case wireGuardObfuscationUDPTCP + case wireGuardObfuscationShadowsocks case wireGuardObfuscationOff case wireGuardObfuscationPort(_ port: UInt16) case quantumResistanceAutomatic @@ -89,11 +93,21 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< } static var wireGuardObfuscation: [Item] { - [.wireGuardObfuscationAutomatic, .wireGuardObfuscationOn, wireGuardObfuscationOff] + var items: [Item] = [ + .wireGuardObfuscationAutomatic, + .wireGuardObfuscationUDPTCP, + .wireGuardObfuscationOff, + ] + + #if DEBUG + items.insert(.wireGuardObfuscationShadowsocks, at: 1) + #endif + + return items } static var wireGuardObfuscationPort: [Item] { - [.wireGuardObfuscationPort(0), wireGuardObfuscationPort(80), wireGuardObfuscationPort(5001)] + [.wireGuardObfuscationPort(0), .wireGuardObfuscationPort(80), .wireGuardObfuscationPort(5001)] } static var quantumResistance: [Item] { @@ -112,8 +126,10 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return .wireGuardCustomPort case .wireGuardObfuscationAutomatic: return .wireGuardObfuscationAutomatic - case .wireGuardObfuscationOn: - return .wireGuardObfuscationOn + case .wireGuardObfuscationUDPTCP: + return .wireGuardObfuscationUDPTCP + case .wireGuardObfuscationShadowsocks: + return .wireGuardObfuscationShadowsocks case .wireGuardObfuscationOff: return .wireGuardObfuscationOff case .wireGuardObfuscationPort: @@ -139,8 +155,10 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return .wireGuardPort case .wireGuardCustomPort: return .wireGuardCustomPort - case .wireGuardObfuscationAutomatic, .wireGuardObfuscationOn, .wireGuardObfuscationOff: + case .wireGuardObfuscationAutomatic, .wireGuardObfuscationOff: return .wireGuardObfuscation + case .wireGuardObfuscationUDPTCP, .wireGuardObfuscationShadowsocks: + return .wireGuardObfuscationOption case .wireGuardObfuscationPort: return .wireGuardObfuscationPort case .quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff: @@ -174,8 +192,9 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< let obfuscationStateItem: Item = switch viewModel.obfuscationState { case .automatic: .wireGuardObfuscationAutomatic case .off: .wireGuardObfuscationOff - case .on: .wireGuardObfuscationOn + case .on: .wireGuardObfuscationUDPTCP } + let quantumResistanceItem: Item = switch viewModel.quantumResistance { case .automatic: .quantumResistanceAutomatic case .off: .quantumResistanceOff @@ -288,9 +307,14 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case .wireGuardObfuscationAutomatic: selectObfuscationState(.automatic) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) - case .wireGuardObfuscationOn: + case .wireGuardObfuscationUDPTCP: selectObfuscationState(.on) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) + // TODO: When ready, add implementation for selected obfuscation. + case .wireGuardObfuscationShadowsocks: + selectObfuscationState(.on) + delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) + // TODO: When ready, add implementation for selected obfuscation. case .wireGuardObfuscationOff: selectObfuscationState(.off) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) @@ -610,6 +634,10 @@ extension VPNSettingsDataSource: VPNSettingsCellEventHandler { delegate?.showInfo(for: button) } + func showDetails(for button: VPNSettingsDetailsButtonItem) { + delegate?.showDetails(for: button) + } + func addCustomPort(_ port: UInt16) { viewModel.setWireGuardPort(port) delegate?.didSelectWireGuardPort(port) diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift index de1a602fce27..98695d1f7fed 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift @@ -17,6 +17,7 @@ protocol DNSSettingsDataSourceDelegate: AnyObject { protocol VPNSettingsDataSourceDelegate: AnyObject { func didUpdateTunnelSettings(_ update: TunnelSettingsUpdate) func showInfo(for: VPNSettingsInfoButtonItem) + func showDetails(for: VPNSettingsDetailsButtonItem) func showDNSSettings() func showIPOverrides() func didSelectWireGuardPort(_ port: UInt16?) diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDetailsButtonItem.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDetailsButtonItem.swift new file mode 100644 index 000000000000..bf4d8636bfc2 --- /dev/null +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDetailsButtonItem.swift @@ -0,0 +1,12 @@ +// +// VPNSettingsDetailsButtonItem.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-10-18. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +enum VPNSettingsDetailsButtonItem { + case udpTcp + case shadowsocks +} diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift index c86db1b9d09b..10a519a737af 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift @@ -111,6 +111,11 @@ extension VPNSettingsViewController: VPNSettingsDataSourceDelegate { alertPresenter.showAlert(presentation: presentation, animated: true) } + func showDetails(for: VPNSettingsDetailsButtonItem) { + // TODO: When ready, add navigation to detail views for selecting obfuscation options for + // UDP-over-TCP and shadowsocks. + } + func showDNSSettings() { let viewController = CustomDNSViewController(interactor: interactor, alertPresenter: alertPresenter) navigationController?.pushViewController(viewController, animated: true)