diff --git a/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift b/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift index c21941f4af41..7dbf88dbb50a 100644 --- a/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift +++ b/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift @@ -32,4 +32,8 @@ public struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource { public func fetchLastReachable() -> PersistentAccessMethod { directAccess } + + public func infoHeaderConfig(for id: UUID) -> InfoHeaderConfig? { + nil + } } diff --git a/ios/MullvadSettings/AccessMethodRepository.swift b/ios/MullvadSettings/AccessMethodRepository.swift index 1ffa3c68e152..3aa2242533b3 100644 --- a/ios/MullvadSettings/AccessMethodRepository.swift +++ b/ios/MullvadSettings/AccessMethodRepository.swift @@ -12,24 +12,28 @@ import MullvadLogging import MullvadTypes public class AccessMethodRepository: AccessMethodRepositoryProtocol { + public static let directId = UUID(uuidString: "C9DB7457-2A55-42C3-A926-C07F82131994")! + public static let bridgeId = UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95084")! + public static let encryptedDNSId = UUID(uuidString: "831CB1F8-1829-42DD-B9DC-82902F298EC0")! + private let logger = Logger(label: "AccessMethodRepository") private let direct = PersistentAccessMethod( - id: UUID(uuidString: "C9DB7457-2A55-42C3-A926-C07F82131994")!, + id: AccessMethodRepository.directId, name: "Direct", isEnabled: true, proxyConfiguration: .direct ) private let bridge = PersistentAccessMethod( - id: UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95084")!, + id: AccessMethodRepository.bridgeId, name: "Mullvad bridges", isEnabled: true, proxyConfiguration: .bridges ) private let encryptedDNS = PersistentAccessMethod( - id: UUID(uuidString: "831CB1F8-1829-42DD-B9DC-82902F298EC0")!, + id: AccessMethodRepository.encryptedDNSId, name: "Encrypted DNS proxy", isEnabled: true, proxyConfiguration: .encryptedDNS diff --git a/ios/MullvadSettings/InfoHeaderConfig.swift b/ios/MullvadSettings/InfoHeaderConfig.swift new file mode 100644 index 000000000000..43ad7bc7e70c --- /dev/null +++ b/ios/MullvadSettings/InfoHeaderConfig.swift @@ -0,0 +1,17 @@ +// +// InfoHeaderConfig.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-10-01. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +public struct InfoHeaderConfig { + public let body: String + public let link: String + + public init(body: String, link: String) { + self.body = body + self.link = link + } +} diff --git a/ios/MullvadSettings/InfoModalConfig.swift b/ios/MullvadSettings/InfoModalConfig.swift new file mode 100644 index 000000000000..e2db6ecfd7a9 --- /dev/null +++ b/ios/MullvadSettings/InfoModalConfig.swift @@ -0,0 +1,19 @@ +// +// InfoModalConfig.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-10-02. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +public struct InfoModalConfig { + public let header: String + public let preamble: String + public let body: [String] + + public init(header: String, preamble: String, body: [String]) { + self.header = header + self.preamble = preamble + self.body = body + } +} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 82506832afa4..594251948617 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -400,7 +400,6 @@ 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF581025D69DB400AEBA94 /* StatusImageView.swift */; }; 58EF87572B16330B00C098B2 /* ProxyConfigurationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF87562B16330B00C098B2 /* ProxyConfigurationTester.swift */; }; 58EF875D2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF875C2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift */; }; - 58EFC76A2AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EFC7692AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift */; }; 58EFC76E2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EFC76D2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift */; }; 58EFC7712AFB45E500E9F4CB /* SettingsChildCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EFC7702AFB45E500E9F4CB /* SettingsChildCoordinator.swift */; }; 58EFC7752AFB4CEF00E9F4CB /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EFC7742AFB4CEF00E9F4CB /* AboutViewController.swift */; }; @@ -466,7 +465,6 @@ 7A1A26492A29D48A00B978AA /* RelayFilterCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A26482A29D48A00B978AA /* RelayFilterCellFactory.swift */; }; 7A21DACF2A30AA3700A787A9 /* UITextField+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */; }; 7A28826A2BA8336600FD9F20 /* VPNSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2882692BA8336600FD9F20 /* VPNSettingsCoordinator.swift */; }; - 7A28826D2BAAC9DE00FD9F20 /* IPOverrideHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A28826C2BAAC9DE00FD9F20 /* IPOverrideHeaderView.swift */; }; 7A2960F62A963F7500389B82 /* AlertCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */; }; 7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */; }; 7A307AD92A8CD8DA0017618B /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A307AD82A8CD8DA0017618B /* Duration.swift */; }; @@ -573,6 +571,9 @@ 7A9CCCC22A96302800DD6A34 /* SafariCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCB02A96302800DD6A34 /* SafariCoordinator.swift */; }; 7A9CCCC32A96302800DD6A34 /* ApplicationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCB12A96302800DD6A34 /* ApplicationCoordinator.swift */; }; 7A9CCCC42A96302800DD6A34 /* TunnelCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCB22A96302800DD6A34 /* TunnelCoordinator.swift */; }; + 7A9F29392CABFAFC005F2089 /* InfoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9F29382CABFAEC005F2089 /* InfoHeaderView.swift */; }; + 7A9F293B2CAC4443005F2089 /* InfoHeaderConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9F293A2CAC4420005F2089 /* InfoHeaderConfig.swift */; }; + 7A9F293D2CAD2FD5005F2089 /* InfoModalConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9F293C2CAD2FCF005F2089 /* InfoModalConfig.swift */; }; 7A9FA1422A2E3306000B728D /* CheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FA1412A2E3306000B728D /* CheckboxView.swift */; }; 7A9FA1442A2E3FE5000B728D /* CheckableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */; }; 7AA513862BC91C6B00D081A4 /* LogRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */; }; @@ -1739,7 +1740,6 @@ 58EF87562B16330B00C098B2 /* ProxyConfigurationTester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyConfigurationTester.swift; sourceTree = ""; }; 58EF875A2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodRepositoryProtocol.swift; sourceTree = ""; }; 58EF875C2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyConfigurationTesterProtocol.swift; sourceTree = ""; }; - 58EFC7692AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessMethodHeaderView.swift; sourceTree = ""; }; 58EFC76D2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessMethodCoordinator.swift; sourceTree = ""; }; 58EFC7702AFB45E500E9F4CB /* SettingsChildCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsChildCoordinator.swift; sourceTree = ""; }; 58EFC7742AFB4CEF00E9F4CB /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; @@ -1796,7 +1796,6 @@ 7A1A264A2A29D65E00B978AA /* SelectableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsCell.swift; sourceTree = ""; }; 7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+Appearance.swift"; sourceTree = ""; }; 7A2882692BA8336600FD9F20 /* VPNSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsCoordinator.swift; sourceTree = ""; }; - 7A28826C2BAAC9DE00FD9F20 /* IPOverrideHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideHeaderView.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 = ""; }; 7A307AD82A8CD8DA0017618B /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = ""; }; @@ -1889,6 +1888,9 @@ 7A9CCCB02A96302800DD6A34 /* SafariCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafariCoordinator.swift; sourceTree = ""; }; 7A9CCCB12A96302800DD6A34 /* ApplicationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationCoordinator.swift; sourceTree = ""; }; 7A9CCCB22A96302800DD6A34 /* TunnelCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelCoordinator.swift; sourceTree = ""; }; + 7A9F29382CABFAEC005F2089 /* InfoHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoHeaderView.swift; sourceTree = ""; }; + 7A9F293A2CAC4420005F2089 /* InfoHeaderConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoHeaderConfig.swift; sourceTree = ""; }; + 7A9F293C2CAD2FCF005F2089 /* InfoModalConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoModalConfig.swift; sourceTree = ""; }; 7A9FA1412A2E3306000B728D /* CheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxView.swift; sourceTree = ""; }; 7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckableSettingsCell.swift; sourceTree = ""; }; 7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRotationTests.swift; sourceTree = ""; }; @@ -2901,6 +2903,7 @@ 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */, 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */, F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */, + 7A9F29382CABFAEC005F2089 /* InfoHeaderView.swift */, 7A5869942B32E9C700640D27 /* LinkButton.swift */, 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */, E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */, @@ -3301,6 +3304,8 @@ F0C13FE32C64F7CB00BD087D /* DAITASettings.swift */, A92ECC2B2A7803A50052F1B1 /* DeviceState.swift */, 580F8B8528197958002E0998 /* DNSSettings.swift */, + 7A9F293A2CAC4420005F2089 /* InfoHeaderConfig.swift */, + 7A9F293C2CAD2FCF005F2089 /* InfoModalConfig.swift */, 7A5869B22B5697AC00640D27 /* IPOverride.swift */, 7A5869BA2B56EE9500640D27 /* IPOverrideRepository.swift */, 06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */, @@ -3569,7 +3574,6 @@ isa = PBXGroup; children = ( 58EFC76D2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift */, - 58EFC7692AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift */, 588D7EDF2AF3A595005DF40A /* ListAccessMethodInteractor.swift */, 588D7EDB2AF3A55E005DF40A /* ListAccessMethodInteractorProtocol.swift */, 588D7EDD2AF3A585005DF40A /* ListAccessMethodItem.swift */, @@ -3760,6 +3764,7 @@ 58FF9FDE2B075AA700E4C97D /* Edit */ = { isa = PBXGroup; children = ( + 5827B0992B0DC0CA00CCBBA1 /* MethodSettings */, 5827B08F2B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift */, 5827B0A52B0F39E900CCBBA1 /* EditAccessMethodInteractor.swift */, 5827B0A32B0F38FD00CCBBA1 /* EditAccessMethodInteractorProtocol.swift */, @@ -3767,7 +3772,6 @@ 58FF9FE12B075BA600E4C97D /* EditAccessMethodSectionIdentifier.swift */, 58FF9FDF2B075ABC00E4C97D /* EditAccessMethodViewController.swift */, 5827B0A92B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift */, - 5827B0992B0DC0CA00CCBBA1 /* MethodSettings */, 5827B0A72B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift */, ); path = Edit; @@ -3796,7 +3800,6 @@ isa = PBXGroup; children = ( 7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */, - 7A28826C2BAAC9DE00FD9F20 /* IPOverrideHeaderView.swift */, 7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */, 7A5869BE2B57D0A100640D27 /* IPOverrideStatus.swift */, 7A5869C02B57D21A00640D27 /* IPOverrideStatusView.swift */, @@ -5451,6 +5454,7 @@ 58B2FDDF2AA71D5C003EB5C6 /* DNSSettings.swift in Sources */, 58B2FDE02AA71D5C003EB5C6 /* TunnelSettings.swift in Sources */, A988DF2A2ADE880300D807EF /* TunnelSettingsV3.swift in Sources */, + 7A9F293D2CAD2FD5005F2089 /* InfoModalConfig.swift in Sources */, 58B2FDE42AA71D5C003EB5C6 /* SettingsManager.swift in Sources */, F0164EBC2B482E430020268D /* AppStorage.swift in Sources */, 58B2FDE62AA71D5C003EB5C6 /* DeviceState.swift in Sources */, @@ -5472,6 +5476,7 @@ 58B2FDE22AA71D5C003EB5C6 /* StoredAccountData.swift in Sources */, F0D7FF902B31E00B00E0FDE5 /* AccessMethodKind.swift in Sources */, 7A5869BC2B56EF3400640D27 /* IPOverrideRepository.swift in Sources */, + 7A9F293B2CAC4443005F2089 /* InfoHeaderConfig.swift in Sources */, F0E61CAB2BF2911D000C4A95 /* MultihopSettings.swift in Sources */, 58B2FDE82AA71D5C003EB5C6 /* KeychainSettingsStore.swift in Sources */, ); @@ -5640,6 +5645,7 @@ 7A5869972B32EA4500640D27 /* AppButton.swift in Sources */, 586C0D8F2B03D88100E7CDD7 /* ProxyProtocolConfigurationItemIdentifier.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, + 7A9F29392CABFAFC005F2089 /* InfoHeaderView.swift in Sources */, 58DFF7D22B0256A300F864E0 /* MarkdownStylingOptions.swift in Sources */, 5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */, F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */, @@ -5806,7 +5812,6 @@ F0C6FA852A6A733700F521F0 /* InAppPurchaseInteractor.swift in Sources */, 58CEB2F92AFD136E00E6E088 /* UIBackgroundConfiguration+Extensions.swift in Sources */, 5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */, - 7A28826D2BAAC9DE00FD9F20 /* IPOverrideHeaderView.swift in Sources */, A98502032B627B120061901E /* LocalNetworkProbe.swift in Sources */, 7A6F2FA92AFD0842006D0856 /* CustomDNSDataSource.swift in Sources */, 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, @@ -5826,7 +5831,6 @@ 5827B0A82B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift in Sources */, 7A5869B92B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift in Sources */, 586C0D7A2B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift in Sources */, - 58EFC76A2AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift in Sources */, 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */, 58FF9FEC2B07A7CB00E4C97D /* NSDirectionalEdgeInsets+Helpers.swift in Sources */, 586C0D832B03D2FF00E7CDD7 /* ShadowsocksSectionHandler.swift in Sources */, diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift index de77f93a43fc..d219a7bf93bc 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift @@ -103,6 +103,24 @@ extension EditAccessMethodCoordinator: EditAccessMethodViewControllerDelegate { onFinish?(self) } + func controllerShouldShowMethodInfo(_ controller: EditAccessMethodViewController, config: InfoModalConfig) { + let aboutController = AboutViewController( + header: config.header, + preamble: config.preamble, + body: config.body + ) + let aboutNavController = UINavigationController(rootViewController: aboutController) + + aboutController.navigationItem.rightBarButtonItem = UIBarButtonItem( + systemItem: .done, + primaryAction: UIAction { [weak aboutNavController] _ in + aboutNavController?.dismiss(animated: true) + } + ) + + navigationController.present(aboutNavController, animated: true) + } + private func getViewModelSubjectFromStore() -> CurrentValueSubject { let persistentMethod = accessMethodRepository.fetch(by: methodIdentifier) return CurrentValueSubject(persistentMethod?.toViewModel() ?? .init()) diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodSectionIdentifier.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodSectionIdentifier.swift index cd8f978113a5..da5926a5914e 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodSectionIdentifier.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodSectionIdentifier.swift @@ -19,14 +19,6 @@ enum EditAccessMethodSectionIdentifier: Hashable { /// The section footer text. var sectionFooter: String? { switch self { - case .enableMethod: - NSLocalizedString( - "ENABLE_METHOD_FOOTER", - tableName: "APIAccess", - value: "When enabled, the app can try to communicate with a Mullvad API server using this method.", - comment: "" - ) - case .testMethod: NSLocalizedString( "TEST_METHOD_FOOTER", @@ -35,7 +27,7 @@ enum EditAccessMethodSectionIdentifier: Hashable { comment: "" ) - case .methodSettings, .cancelTest, .testingStatus, .deleteMethod: + case .enableMethod, .methodSettings, .cancelTest, .testingStatus, .deleteMethod: nil } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift index f5158ebed50b..c3f73d7de992 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift @@ -7,15 +7,17 @@ // import Combine +import MullvadSettings import UIKit /// The view controller providing the interface for editing the existing access method. -class EditAccessMethodViewController: UITableViewController { +class EditAccessMethodViewController: UIViewController { typealias EditAccessMethodDataSource = UITableViewDiffableDataSource< EditAccessMethodSectionIdentifier, EditAccessMethodItemIdentifier > + private let tableView = UITableView(frame: .zero, style: .insetGrouped) private let subject: CurrentValueSubject private let interactor: EditAccessMethodInteractorProtocol private var alertPresenter: AlertPresenter @@ -33,7 +35,7 @@ class EditAccessMethodViewController: UITableViewController { self.interactor = interactor self.alertPresenter = alertPresenter - super.init(style: .insetGrouped) + super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { @@ -45,11 +47,19 @@ class EditAccessMethodViewController: UITableViewController { view.accessibilityIdentifier = .editAccessMethodView view.backgroundColor = .secondaryColor + tableView.backgroundColor = .secondaryColor - navigationItem.largeTitleDisplayMode = .never + tableView.delegate = self isModalInPresentation = true + let headerView = createHeaderView() + view.addConstrainedSubviews([headerView, tableView]) { + headerView.pinEdgesToSuperviewMargins(PinnableEdges([.leading(8), .trailing(8), .top(0)])) + tableView.pinEdgesToSuperview(.all().excluding(.top)) + tableView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 20) + } + configureDataSource() configureNavigationItem() } @@ -59,15 +69,32 @@ class EditAccessMethodViewController: UITableViewController { interactor.cancelProxyConfigurationTest() } - // MARK: - UITableViewDelegate + private func createHeaderView() -> UIView { + var headerView: InfoHeaderView? + + if let headerConfig = subject.value.infoHeaderConfig { + headerView = InfoHeaderView(config: headerConfig) + + headerView?.onAbout = { [weak self] in + guard let self, let infoModalConfig = subject.value.infoModalConfig else { return } + delegate?.controllerShouldShowMethodInfo(self, config: infoModalConfig) + } + } + + return headerView ?? UIView() + } +} + +// MARK: - UITableViewDelegate - override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { +extension EditAccessMethodViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return false } return itemIdentifier.isSelectable } - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return } if case .methodSettings = itemIdentifier { @@ -75,29 +102,29 @@ class EditAccessMethodViewController: UITableViewController { } } - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UIMetrics.SettingsCell.apiAccessCellHeight } - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { return nil } // Header height shenanigans to avoid extra spacing in testing sections when testing is NOT ongoing. - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return 0 } switch sectionIdentifier { - case .enableMethod, .methodSettings, .deleteMethod, .testMethod: + case .methodSettings, .deleteMethod, .testMethod: return UITableView.automaticDimension case .testingStatus: return subject.value.testingStatus == .initial ? 0 : UITableView.automaticDimension - case .cancelTest: + case .enableMethod, .cancelTest: return 0 } } - override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return nil } guard let sectionFooterText = sectionIdentifier.sectionFooter else { return nil } @@ -114,7 +141,7 @@ class EditAccessMethodViewController: UITableViewController { } // Footer height shenanigans to avoid extra spacing in testing sections when testing is NOT ongoing. - override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return 0 } let marginToDeleteMethodItem: CGFloat = 24 @@ -377,4 +404,4 @@ class EditAccessMethodViewController: UITableViewController { private func onCancelTest() { interactor.cancelProxyConfigurationTest() } -} +} // swiftlint:disable:this file_length diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift index 9c0f8447864e..cd44f48661ed 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadSettings protocol EditAccessMethodViewControllerDelegate: AnyObject, AccessMethodEditing { /// The view controller requests the delegate to present the proxy configuration view controller. @@ -19,4 +20,8 @@ protocol EditAccessMethodViewControllerDelegate: AnyObject, AccessMethodEditing /// /// - Parameter controller: the calling controller. func controllerDidDeleteAccessMethod(_ controller: EditAccessMethodViewController) + + /// The view controller requests the delegate to present information about the access method. + /// - Parameter controller: the calling controller. + func controllerShouldShowMethodInfo(_ controller: EditAccessMethodViewController, config: InfoModalConfig) } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodHeaderView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodHeaderView.swift deleted file mode 100644 index a65ceaea7924..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodHeaderView.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// ListAccessMethodHeaderView.swift -// MullvadVPN -// -// Created by pronebird on 07/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -/// Header view pinned at the top of ``AccessMethodListViewController``. -class ListAccessMethodHeaderView: UIView, UITextViewDelegate { - /// Event handler invoked when user taps on the link to learn more about API access. - var onAbout: (() -> Void)? - - private let textView = UITextView() - - override init(frame: CGRect) { - super.init(frame: frame) - - textView.backgroundColor = .clear - textView.dataDetectorTypes = .link - textView.isSelectable = true - textView.isEditable = false - textView.isScrollEnabled = false - textView.contentInset = .zero - textView.textContainerInset = .zero - textView.attributedText = makeAttributedString() - textView.linkTextAttributes = defaultLinkAttributes - textView.textContainer.lineFragmentPadding = 0 - textView.delegate = self - - directionalLayoutMargins = UIMetrics.contentHeadingLayoutMargins - - addSubviews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private let defaultTextAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.systemFont(ofSize: 13), - .foregroundColor: UIColor.ContentHeading.textColor, - ] - - private let defaultLinkAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.systemFont(ofSize: 13), - .foregroundColor: UIColor.ContentHeading.linkColor, - ] - - private func makeAttributedString() -> NSAttributedString { - let body = NSLocalizedString( - "ACCESS_METHOD_HEADER_BODY", - tableName: "APIAccess", - value: "Manage default and setup custom methods to access the Mullvad API.", - comment: "" - ) - let link = NSLocalizedString( - "ACCESS_METHOD_HEADER_LINK", - tableName: "APIAccess", - value: "About API access...", - comment: "" - ) - - var linkAttributes = defaultLinkAttributes - linkAttributes[.link] = "#" - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineBreakMode = .byWordWrapping - - let attributedString = NSMutableAttributedString() - attributedString.append(NSAttributedString(string: body, attributes: defaultTextAttributes)) - attributedString.append(NSAttributedString(string: " ", attributes: defaultTextAttributes)) - attributedString.append(NSAttributedString(string: link, attributes: linkAttributes)) - attributedString.addAttribute( - .paragraphStyle, - value: paragraphStyle, - range: NSRange(location: 0, length: attributedString.length) - ) - return attributedString - } - - private func addSubviews() { - addConstrainedSubviews([textView]) { - textView.pinEdgesToSuperviewMargins() - } - } - - func textView( - _ textView: UITextView, - shouldInteractWith URL: URL, - in characterRange: NSRange, - interaction: UITextItemInteraction - ) -> Bool { - onAbout?() - return false - } - - @available(iOS 17.0, *) - func textView(_ textView: UITextView, menuConfigurationFor textItem: UITextItem, defaultMenu: UIMenu) -> UITextItem - .MenuConfiguration? { - return nil - } - - @available(iOS 17.0, *) - func textView(_ textView: UITextView, primaryActionFor textItem: UITextItem, defaultAction: UIAction) -> UIAction? { - if case .link = textItem.content { - return UIAction { [weak self] _ in - self?.onAbout?() - } - } - return nil - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift index 474baea1f7fd..8c0facf98d96 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift @@ -26,7 +26,6 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { ListAccessMethodItemIdentifier > - private let headerView = ListAccessMethodHeaderView() private let interactor: ListAccessMethodInteractorProtocol private var lastReachableMethodItem: ListAccessMethodItem? private var cancellables = Set() @@ -54,10 +53,6 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { override func viewDidLoad() { super.viewDidLoad() - headerView.onAbout = { [weak self] in - self?.sendAbout() - } - view.backgroundColor = .secondaryColor tableView.delegate = self @@ -69,10 +64,11 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { view.accessibilityIdentifier = .apiAccessView + let headerView = createHeaderView() view.addConstrainedSubviews([headerView, tableView]) { - headerView.pinEdgesToSuperview(.all().excluding(.bottom)) + headerView.pinEdgesToSuperviewMargins(PinnableEdges([.leading(8), .trailing(8), .top(0)])) tableView.pinEdgesToSuperview(.all().excluding(.top)) - tableView.topAnchor.constraint(equalTo: headerView.bottomAnchor) + tableView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 20) } addChild(contentController) @@ -147,6 +143,29 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { ) } + private func createHeaderView() -> InfoHeaderView { + let body = NSLocalizedString( + "ACCESS_METHOD_HEADER_BODY", + tableName: "APIAccess", + value: "Manage default and setup custom methods to access the Mullvad API.", + comment: "" + ) + let link = NSLocalizedString( + "ACCESS_METHOD_HEADER_LINK", + tableName: "APIAccess", + value: "About API access...", + comment: "" + ) + + let headerView = InfoHeaderView(config: InfoHeaderConfig(body: body, link: link)) + + headerView.onAbout = { [weak self] in + self?.sendAbout() + } + + return headerView + } + private func configureDataSource() { dataSource = ListAccessMethodDataSource( tableView: tableView, diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel.swift index 279fa966ebdc..8adab8c5a94f 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel.swift @@ -72,3 +72,166 @@ struct AccessMethodViewModel: Identifiable { /// Shadowsocks configuration view model. var shadowsocks = Shadowsocks() } + +extension AccessMethodViewModel { + var infoHeaderConfig: InfoHeaderConfig? { + switch id { + case AccessMethodRepository.directId: + InfoHeaderConfig( + body: NSLocalizedString( + "DIRECT_ACCESS_METHOD_HEADER_BODY", + tableName: "APIAccess", + value: "The app communicates with a Mullvad API server directly.", + comment: "" + ), + link: NSLocalizedString( + "DIRECT_ACCESS_METHOD_HEADER_LINK", + tableName: "APIAccess", + value: "About Direct method...", + comment: "" + ) + ) + case AccessMethodRepository.bridgeId: + InfoHeaderConfig( + body: NSLocalizedString( + "BRIDGES_ACCESS_METHOD_HEADER_BODY", + tableName: "APIAccess", + value: "The app communicates with a Mullvad API server via a Mullvad bridge server.", + comment: "" + ), + link: NSLocalizedString( + "BRIDGES_ACCESS_METHOD_HEADER_LINK", + tableName: "APIAccess", + value: "About Mullvad bridges method...", + comment: "" + ) + ) + case AccessMethodRepository.encryptedDNSId: + InfoHeaderConfig( + body: NSLocalizedString( + "ENCRYPTED_DNS_ACCESS_METHOD_HEADER_BODY", + tableName: "APIAccess", + value: "The app communicates with a Mullvad API server via a proxy address.", + comment: "" + ), + link: NSLocalizedString( + "ENCRYPTED_DNS_ACCESS_METHOD_HEADER_LINK", + tableName: "APIAccess", + value: "About Encrypted DNS proxy method...", + comment: "" + ) + ) + default: + nil + } + } + + var infoModalConfig: InfoModalConfig? { + switch id { + case AccessMethodRepository.directId: + InfoModalConfig( + header: NSLocalizedString( + "DIRECT_ACCESS_METHOD_MODAL_HEADER", + tableName: "APIAccess", + value: "Direct", + comment: "" + ), + preamble: NSLocalizedString( + "DIRECT_ACCESS_METHOD_MODAL_PREAMBLE", + tableName: "APIAccess", + value: "The app communicates with a Mullvad API server directly.", + comment: "" + ), + body: [ + NSLocalizedString( + "DIRECT_ACCESS_METHOD_MODAL_BODY_PART_1", + tableName: "APIAccess", + value: """ + With the “Direct” method, the app communicates with a Mullvad API server \ + directly without any intermediate proxies. + """, + comment: "" + ), + NSLocalizedString( + "DIRECT_ACCESS_METHOD_MODAL_BODY_PART_2", + tableName: "APIAccess", + value: "This can be useful when you are not affected by censorship.", + comment: "" + ), + ] + ) + case AccessMethodRepository.bridgeId: + InfoModalConfig( + header: NSLocalizedString( + "BRIDGES_ACCESS_METHOD_MODAL_HEADER", + tableName: "APIAccess", + value: "Mullvad bridges", + comment: "" + ), + preamble: NSLocalizedString( + "BRIDGES_ACCESS_METHOD_MODAL_PREAMBLE", + tableName: "APIAccess", + value: "The app communicates with a Mullvad API server via a Mullvad bridge server.", + comment: "" + ), + body: [ + NSLocalizedString( + "BRIDGES_ACCESS_METHOD_MODAL_BODY_PART_1", + tableName: "APIAccess", + value: """ + With the “Mullvad bridges” method, the app communicates with a Mullvad API server via a \ + Mullvad bridge server. It does this by sending the traffic obfuscated by Shadowsocks. + """, + comment: "" + ), + NSLocalizedString( + "BRIDGES_ACCESS_METHOD_MODAL_BODY_PART_2", + tableName: "APIAccess", + value: "This can be useful if the API is censored but Mullvad’s bridge servers are not.", + comment: "" + ), + ] + ) + case AccessMethodRepository.encryptedDNSId: + InfoModalConfig( + header: NSLocalizedString( + "ENCRYPTED_DNS_ACCESS_METHOD_MODAL_HEADER", + tableName: "APIAccess", + value: "Encrypted DNS proxy", + comment: "" + ), + preamble: NSLocalizedString( + "ENCRYPTED_DNS_ACCESS_METHOD_MODAL_PREAMBLE", + tableName: "APIAccess", + value: "The app communicates with a Mullvad API server via a proxy address.", + comment: "" + ), + body: [ + NSLocalizedString( + "ENCRYPTED_DNS_ACCESS_METHOD_MODAL_BODY_PART_1", + tableName: "APIAccess", + value: """ + With the “Encrypted DNS proxy” method, the app will communicate with our \ + Mullvad API through a proxy address. + It does this by retrieving an address from a DNS over HTTPS (DoH) server and \ + then using that to reach our API servers. + """, + comment: "" + ), + NSLocalizedString( + "ENCRYPTED_DNS_ACCESS_METHOD_MODAL_BODY_PART_2", + tableName: "APIAccess", + value: """ + If you are not connected to our VPN, then the Encrypted DNS proxy will use your own non-VPN IP \ + when connecting. + The DoH servers are hosted by one of the following providers: Quad9, Cloudflare, or Google. + """, + comment: "" + ), + ] + ) + default: + nil + } + } +} diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift index be9116823d27..8a13306febe0 100644 --- a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift @@ -7,13 +7,13 @@ // import Combine +import MullvadSettings import UIKit class IPOverrideViewController: UIViewController { private let interactor: IPOverrideInteractor private var cancellables = Set() private let alertPresenter: AlertPresenter - private let headerView = IPOverrideHeaderView() weak var delegate: IPOverrideViewControllerDelegate? @@ -81,6 +81,21 @@ class IPOverrideViewController: UIViewController { } private func addHeaderView() { + let body = NSLocalizedString( + "ACCESS_METHOD_HEADER_BODY", + tableName: "APIAccess", + value: "Manage default and setup custom methods to access the Mullvad API.", + comment: "" + ) + let link = NSLocalizedString( + "ACCESS_METHOD_HEADER_LINK", + tableName: "APIAccess", + value: "About API access...", + comment: "" + ) + + let headerView = InfoHeaderView(config: InfoHeaderConfig(body: body, link: link)) + headerView.onAbout = { [weak self] in self?.delegate?.presentAbout() } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift index 5ce99f689f5a..32facd0ecab8 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift @@ -128,7 +128,6 @@ extension VPNSettingsViewController: VPNSettingsDataSourceDelegate { interactor.evaluateDaitaSettingsCompatibility(settings) } - // swiftlint:disable:next function_body_length func showPrompt( for item: VPNSettingsPromptAlertItem, onSave: @escaping () -> Void, diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideHeaderView.swift b/ios/MullvadVPN/Views/InfoHeaderView.swift similarity index 73% rename from ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideHeaderView.swift rename to ios/MullvadVPN/Views/InfoHeaderView.swift index 16c751f121d1..67564bc312a0 100644 --- a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideHeaderView.swift +++ b/ios/MullvadVPN/Views/InfoHeaderView.swift @@ -1,22 +1,26 @@ // -// IPOverrideHeaderView.swift +// InfoHeaderView.swift // MullvadVPN // -// Created by Jon Petersson on 2024-03-20. +// Created by Jon Petersson on 2024-10-01. // Copyright © 2024 Mullvad VPN AB. All rights reserved. // +import MullvadSettings import UIKit -/// Header view pinned at the top of ``IPOverrideViewController``. -class IPOverrideHeaderView: UIView, UITextViewDelegate { - /// Event handler invoked when user taps on the link to learn more about API access. +/// Header view pinned at the top of a ``ViewController``. +class InfoHeaderView: UIView, UITextViewDelegate { + /// Event handler invoked when user taps on the link. var onAbout: (() -> Void)? private let textView = UITextView() + private let config: InfoHeaderConfig - override init(frame: CGRect) { - super.init(frame: frame) + init(config: InfoHeaderConfig) { + self.config = config + + super.init(frame: .zero) textView.backgroundColor = .clear textView.dataDetectorTypes = .link @@ -50,19 +54,6 @@ class IPOverrideHeaderView: UIView, UITextViewDelegate { ] private func makeAttributedString() -> NSAttributedString { - let body = NSLocalizedString( - "IP_OVERRIDE_HEADER_BODY", - tableName: "IPOverride", - value: "Import files or text with new IP addresses for the servers in the Select location view.", - comment: "" - ) - let link = NSLocalizedString( - "IP_OVERRIDE_HEADER_LINK", - tableName: "IPOverride", - value: "About IP override...", - comment: "" - ) - var linkAttributes = defaultLinkAttributes linkAttributes[.link] = "#" @@ -70,9 +61,9 @@ class IPOverrideHeaderView: UIView, UITextViewDelegate { paragraphStyle.lineBreakMode = .byWordWrapping let attributedString = NSMutableAttributedString() - attributedString.append(NSAttributedString(string: body, attributes: defaultTextAttributes)) + attributedString.append(NSAttributedString(string: config.body, attributes: defaultTextAttributes)) attributedString.append(NSAttributedString(string: " ", attributes: defaultTextAttributes)) - attributedString.append(NSAttributedString(string: link, attributes: linkAttributes)) + attributedString.append(NSAttributedString(string: config.link, attributes: linkAttributes)) attributedString.addAttribute( .paragraphStyle, value: paragraphStyle,