diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 60a74fa1b5ef..8a0abdefd0ec 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -505,6 +505,8 @@ 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; }; 7A5869952B32E9C700640D27 /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869942B32E9C700640D27 /* LinkButton.swift */; }; 7A5869972B32EA4500640D27 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869962B32EA4500640D27 /* AppButton.swift */; }; + 7A5869AB2B55527C00640D27 /* IPOverrideCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */; }; + 7A5869AD2B5552E200640D27 /* IPOverrideViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */; }; 7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; }; 7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; }; 7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; }; @@ -1623,7 +1625,6 @@ 58FF9FF32B07C61B00E4C97D /* AccessMethodValidationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodValidationError.swift; sourceTree = ""; }; 7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNScreenshots.xctestplan; sourceTree = ""; }; 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = ""; }; - 7A0B31152B2B4BE7004B12E0 /* AccessbilityIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessbilityIdentifier.swift; sourceTree = ""; }; 7A0B311D2B303A0D004B12E0 /* AccessbilityIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessbilityIdentifier.swift; sourceTree = ""; }; 7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coordinator+Router.swift"; sourceTree = ""; }; 7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionProtocol.swift; sourceTree = ""; }; @@ -1645,6 +1646,8 @@ 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = ""; }; 7A5869942B32E9C700640D27 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = ""; }; 7A5869962B32EA4500640D27 /* AppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = ""; }; + 7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideCoordinator.swift; sourceTree = ""; }; + 7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideViewController.swift; sourceTree = ""; }; 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = ""; }; 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = ""; }; 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = ""; }; @@ -2016,7 +2019,6 @@ 581943F228F8014500B0CB5E /* MullvadTypes */ = { isa = PBXGroup; children = ( - 7A0B31152B2B4BE7004B12E0 /* AccessbilityIdentifier.swift */, 584D26BE270C550B004EA533 /* AnyIPAddress.swift */, 586A951329013235007BAF2B /* AnyIPEndpoint.swift */, 06AC113628F83FD70037AF9A /* Cancellable.swift */, @@ -3151,6 +3153,7 @@ isa = PBXGroup; children = ( 58CEB2E72AFBB9F300E6E088 /* APIAccess */, + 7A5869A92B55516700640D27 /* IPOverride */, 58EFC7702AFB45E500E9F4CB /* SettingsChildCoordinator.swift */, 7A9CCCAD2A96302800DD6A34 /* SettingsCoordinator.swift */, ); @@ -3218,6 +3221,15 @@ path = Alert; sourceTree = ""; }; + 7A5869A92B55516700640D27 /* IPOverride */ = { + isa = PBXGroup; + children = ( + 7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */, + 7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */, + ); + path = IPOverride; + sourceTree = ""; + }; 7A83C3FC2A55B39500DFB83A /* TestPlans */ = { isa = PBXGroup; children = ( @@ -4612,6 +4624,7 @@ 58DFF7D82B02774C00F864E0 /* ListItemPickerViewController.swift in Sources */, 5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */, 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */, + 7A5869AB2B55527C00640D27 /* IPOverrideCoordinator.swift in Sources */, 5867771429097BCD006F721F /* PaymentState.swift in Sources */, F0EF50D32A8FA47E0031E8DF /* ChangeLogInteractor.swift in Sources */, 7AC8A3AF2ABC71D600DC4939 /* TermsOfServiceCoordinator.swift in Sources */, @@ -4837,6 +4850,7 @@ 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */, 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */, 58FF9FEA2B07653800E4C97D /* ButtonCellContentView.swift in Sources */, + 7A5869AD2B5552E200640D27 /* IPOverrideViewController.swift in Sources */, F0E8E4C12A602CCB00ED26A3 /* AccountDeletionContentView.swift in Sources */, 58EF87512B16176300C098B2 /* AccessMethodActionSheetContentConfiguration.swift in Sources */, 58B26E1E2943514300D5980C /* InAppNotificationDescriptor.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 340762d78754..a9e29c6ff432 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -34,6 +34,7 @@ public enum AccessibilityIdentifier: String { case problemReportCell case faqCell case apiAccessCell + case ipOverrideCell case relayFilterOwnershipCell case relayFilterProviderCell diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index 077214fc391f..a7ccb99e6149 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -159,7 +159,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo presentTOS(animated: animated, completion: completion) case .main: - presentMain(animated: animated, completion: completion) +// presentMain(animated: animated, completion: completion) + presentSettings(route: .ipOverride, animated: false, completion: completion) case .welcome: presentWelcome(animated: animated, completion: completion) diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift new file mode 100644 index 000000000000..8ba9a072a412 --- /dev/null +++ b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift @@ -0,0 +1,28 @@ +// +// IPOverrideCoordinator.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-01-15. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import Routing +import UIKit + +class IPOverrideCoordinator: Coordinator, Presenting, SettingsChildCoordinator { + let navigationController: UINavigationController + + var presentationContext: UIViewController { + navigationController + } + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func start(animated: Bool) { + let viewController = IPOverrideViewController(alertPresenter: AlertPresenter(context: self)) + navigationController.pushViewController(viewController, animated: animated) + } +} diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift new file mode 100644 index 000000000000..80a675b93645 --- /dev/null +++ b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift @@ -0,0 +1,225 @@ +// +// IPOverrideViewController.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-01-15. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class IPOverrideViewController: UIViewController { + let alertPresenter: AlertPresenter + + private lazy var containerView: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.spacing = 20 + return view + }() + + private lazy var clearButton: AppButton = { + let button = AppButton(style: .danger) + button.addTarget(self, action: #selector(didTapClearButton), for: .touchUpInside) + button.setTitle(NSLocalizedString( + "IP_OVERRIDE_CLEAR_BUTTON", + tableName: "IPOverride", + value: "Clear all overrides", + comment: "" + ), for: .normal) + return button + }() + + init(alertPresenter: AlertPresenter) { + self.alertPresenter = alertPresenter + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationController?.navigationBar.prefersLargeTitles = false + view.backgroundColor = .secondaryColor + + addHeader() + addPreamble() + addImportButtons() + addStatusLabel() + + view.addConstrainedSubviews([containerView, clearButton]) { + containerView.pinEdgesToSuperviewMargins(.all().excluding(.bottom)) + clearButton.pinEdgesToSuperviewMargins(.all().excluding(.top)) + } + } + + private func addHeader() { + let label = UILabel() + label.font = .systemFont(ofSize: 32, weight: .bold) + label.textColor = .white + label.text = NSLocalizedString( + "IP_OVERRIDE_HEADER", + tableName: "IPOverride", + value: "Server IP override", + comment: "" + ) + + let infoButton = UIButton(type: .custom) + infoButton.tintColor = .white + infoButton.setImage(UIImage(resource: .iconInfo), for: .normal) + infoButton.addTarget(self, action: #selector(didTapInfoButton), for: .touchUpInside) + infoButton.heightAnchor.constraint(equalToConstant: 24).isActive = true + infoButton.widthAnchor.constraint(equalTo: infoButton.heightAnchor, multiplier: 1).isActive = true + + let headerView = UIStackView(arrangedSubviews: [label, infoButton, UIView()]) + headerView.spacing = 8 + + containerView.addArrangedSubview(headerView) + containerView.setCustomSpacing(14, after: headerView) + } + + private func addPreamble() { + let label = UILabel() + label.font = .systemFont(ofSize: 12, weight: .semibold) + label.textColor = .white.withAlphaComponent(0.6) + label.numberOfLines = 0 + label.text = NSLocalizedString( + "IP_OVERRIDE_PREAMBLE", + tableName: "IPOverride", + value: "Import files or text with new IP addresses for the servers in the Select location view.", + comment: "" + ) + + containerView.addArrangedSubview(label) + } + + private func addImportButtons() { + let importTextButton = AppButton(style: .default) + importTextButton.addTarget(self, action: #selector(didTapImportTextButton), for: .touchUpInside) + importTextButton.setTitle(NSLocalizedString( + "IP_OVERRIDE_IMPORT_TEXT_BUTTON", + tableName: "IPOverride", + value: "Import via text", + comment: "" + ), for: .normal) + + let importFileButton = AppButton(style: .default) + importFileButton.addTarget(self, action: #selector(didTapImportFileButton), for: .touchUpInside) + importFileButton.setTitle(NSLocalizedString( + "IP_OVERRIDE_IMPORT_FILE_BUTTON", + tableName: "IPOverride", + value: "Import file", + comment: "" + ), for: .normal) + + let stackView = UIStackView(arrangedSubviews: [importTextButton, importFileButton]) + stackView.distribution = .fillEqually + stackView.spacing = 12 + + containerView.addArrangedSubview(stackView) + } + + private func addStatusLabel() { + let label = UILabel() + label.font = .systemFont(ofSize: 22, weight: .bold) + label.textColor = .white + label.text = NSLocalizedString( + "IP_OVERRIDE_STATUS", + tableName: "IPOverride", + value: "Overrides active", + comment: "" + ).uppercased() + + containerView.addArrangedSubview(label) + } + + @objc private func didTapInfoButton() { + let message = NSLocalizedString( + "IP_OVERRIDE_DIALOG_MESSAGE", + tableName: "IPOverride", + value: """ + On some networks, where various types of censorship are being used, our server IP addresses are \ + sometimes blocked. + + To circumvent this you can import a file or a text, provided by our support team, \ + with new IP addresses that override the default addresses of the servers in the Select location view. + + If you are having issues connecting to VPN servers, please contact support. + """, + comment: "" + ) + + let presentation = AlertPresentation( + id: "ip-override-info-alert", + icon: .info, + title: NSLocalizedString( + "IP_OVERRIDE_INFO_DIALOG_TITLE", + tableName: "IPOverride", + value: "Server IP override", + comment: "" + ), + message: message, + buttons: [AlertAction( + title: NSLocalizedString( + "IP_OVERRIDE_INFO_DIALOG_OK_BUTTON", + tableName: "IPOverride", + value: "Got it!", + comment: "" + ), + style: .default + )] + ) + + alertPresenter.showAlert(presentation: presentation, animated: true) + } + + @objc private func didTapClearButton() { + let presentation = AlertPresentation( + id: "ip-override-clear-alert", + icon: .alert, + title: NSLocalizedString( + "IP_OVERRIDE_CLEAR_DIALOG_TITLE", + tableName: "IPOverride", + value: "Clear all overrides?", + comment: "" + ), + message: NSLocalizedString( + "IP_OVERRIDE_CLEAR_DIALOG_MESSAGE", + tableName: "IPOverride", + value: """ + Clearing the imported overrides changes the server IPs, in the Select location view, \ + back to default. + """, + comment: "" + ), + buttons: [ + AlertAction( + title: NSLocalizedString( + "IP_OVERRIDE_CLEAR_DIALOG_CANCEL_BUTTON", + tableName: "IPOverride", + value: "Cancel", + comment: "" + ), + style: .default + ), + AlertAction( + title: NSLocalizedString( + "IP_OVERRIDE_CLEAR_DIALOG_CLEAR_BUTTON", + tableName: "IPOverride", + value: "Clear", + comment: "" + ), + style: .destructive + ), + ] + ) + + alertPresenter.showAlert(presentation: presentation, animated: true) + } + + @objc private func didTapImportTextButton() {} + @objc private func didTapImportFileButton() {} +} diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift index 4d9abb81f868..023e4f880ca6 100644 --- a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift @@ -27,6 +27,9 @@ enum SettingsNavigationRoute: Equatable { /// API access route. case apiAccess + + /// IP override route. + case ipOverride } /// Top-level settings coordinator. @@ -248,6 +251,9 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV case .apiAccess: return .childCoordinator(ListAccessMethodCoordinator(navigationController: navigationController)) + case .ipOverride: + return .childCoordinator(IPOverrideCoordinator(navigationController: navigationController)) + case .faq: // Handled separately and presented as a modal. return .failed @@ -267,6 +273,8 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV return .problemReport case is ListAccessMethodViewController: return .apiAccess + case is IPOverrideViewController: + return .ipOverride default: return nil } diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift index 4cd7884aef5b..7f5f2d02a692 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift @@ -92,6 +92,19 @@ struct SettingsCellFactory: CellFactoryProtocol { cell.detailTitleLabel.text = nil cell.accessibilityIdentifier = item.accessibilityIdentifier cell.disclosureType = .chevron + + case .ipOverride: + guard let cell = cell as? SettingsCell else { return } + + cell.titleLabel.text = NSLocalizedString( + "IP_OVERRIDE_CELL_LABEL", + tableName: "Settings", + value: "Server IP override", + comment: "" + ) + cell.detailTitleLabel.text = nil + cell.accessibilityIdentifier = item.accessibilityIdentifier + cell.disclosureType = .chevron } } } diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift index a7615bdf9830..71c493b7c82f 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift @@ -39,6 +39,7 @@ final class SettingsDataSource: UITableViewDiffableDataSource