Skip to content

Commit

Permalink
Add preliminary settings page for relay IP overrides
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson authored and buggmagnet committed Jan 23, 2024
1 parent 3a74932 commit 7ad86ce
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 2 deletions.
18 changes: 16 additions & 2 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1643,7 +1645,6 @@
58FF9FF32B07C61B00E4C97D /* AccessMethodValidationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodValidationError.swift; sourceTree = "<group>"; };
7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNScreenshots.xctestplan; sourceTree = "<group>"; };
7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = "<group>"; };
7A0B31152B2B4BE7004B12E0 /* AccessbilityIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessbilityIdentifier.swift; sourceTree = "<group>"; };
7A0B311D2B303A0D004B12E0 /* AccessbilityIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessbilityIdentifier.swift; sourceTree = "<group>"; };
7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coordinator+Router.swift"; sourceTree = "<group>"; };
7A12D0752B062D5C00E9602D /* URLSessionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionProtocol.swift; sourceTree = "<group>"; };
Expand All @@ -1665,6 +1666,8 @@
7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = "<group>"; };
7A5869942B32E9C700640D27 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = "<group>"; };
7A5869962B32EA4500640D27 /* AppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = "<group>"; };
7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideCoordinator.swift; sourceTree = "<group>"; };
7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideViewController.swift; sourceTree = "<group>"; };
7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = "<group>"; };
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = "<group>"; };
7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2060,7 +2063,6 @@
581943F228F8014500B0CB5E /* MullvadTypes */ = {
isa = PBXGroup;
children = (
7A0B31152B2B4BE7004B12E0 /* AccessbilityIdentifier.swift */,
584D26BE270C550B004EA533 /* AnyIPAddress.swift */,
586A951329013235007BAF2B /* AnyIPEndpoint.swift */,
06AC113628F83FD70037AF9A /* Cancellable.swift */,
Expand Down Expand Up @@ -3200,6 +3202,7 @@
isa = PBXGroup;
children = (
58CEB2E72AFBB9F300E6E088 /* APIAccess */,
7A5869A92B55516700640D27 /* IPOverride */,
58EFC7702AFB45E500E9F4CB /* SettingsChildCoordinator.swift */,
7A9CCCAD2A96302800DD6A34 /* SettingsCoordinator.swift */,
);
Expand Down Expand Up @@ -3269,6 +3272,15 @@
path = Alert;
sourceTree = "<group>";
};
7A5869A92B55516700640D27 /* IPOverride */ = {
isa = PBXGroup;
children = (
7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */,
7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */,
);
path = IPOverride;
sourceTree = "<group>";
};
7A83C3FC2A55B39500DFB83A /* TestPlans */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4723,6 +4735,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 */,
Expand Down Expand Up @@ -4948,6 +4961,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 */,
Expand Down
1 change: 1 addition & 0 deletions ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public enum AccessibilityIdentifier: String {
case problemReportCell
case faqCell
case apiAccessCell
case ipOverrideCell
case relayFilterOwnershipCell
case relayFilterProviderCell

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ enum SettingsNavigationRoute: Equatable {

/// API access route.
case apiAccess

/// IP override route.
case ipOverride
}

/// Top-level settings coordinator.
Expand Down Expand Up @@ -255,6 +258,9 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV
accessMethodRepository: accessMethodRepository
))

case .ipOverride:
return .childCoordinator(IPOverrideCoordinator(navigationController: navigationController))

case .faq:
// Handled separately and presented as a modal.
return .failed
Expand All @@ -274,6 +280,8 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV
return .problemReport
case is ListAccessMethodViewController:
return .apiAccess
case is IPOverrideViewController:
return .ipOverride
default:
return nil
}
Expand Down
13 changes: 13 additions & 0 deletions ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Loading

0 comments on commit 7ad86ce

Please sign in to comment.