diff options
6 files changed, 170 insertions, 73 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 5025450b41..2d714acb6d 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -486,6 +486,7 @@ 7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */; }; 7A307AD92A8CD8DA0017618B /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A307AD82A8CD8DA0017618B /* Duration.swift */; }; 7A307ADB2A8F56DF0017618B /* Duration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */; }; + 7A3215742D3E5A85005DF395 /* DAITASettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3215732D3E5A7B005DF395 /* DAITASettingsCoordinator.swift */; }; 7A33538F2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A33538E2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift */; }; 7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */; }; 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; }; @@ -1891,6 +1892,7 @@ 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresentation.swift; sourceTree = "<group>"; }; 7A307AD82A8CD8DA0017618B /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = "<group>"; }; 7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extensions.swift"; sourceTree = "<group>"; }; + 7A3215732D3E5A7B005DF395 /* DAITASettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITASettingsCoordinator.swift; sourceTree = "<group>"; }; 7A33538E2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProviderManager.swift; sourceTree = "<group>"; }; 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPNConnection.swift; sourceTree = "<group>"; }; 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = "<group>"; }; @@ -4087,6 +4089,7 @@ 7A8A19082CE5FFD7000BCB5B /* DAITA */ = { isa = PBXGroup; children = ( + 7A3215732D3E5A7B005DF395 /* DAITASettingsCoordinator.swift */, F041BE4E2C983C2B0083EC28 /* DAITASettingsPromptItem.swift */, 7A8A19132CEF2527000BCB5B /* DAITATunnelSettingsViewModel.swift */, 7A8A19092CE5FFDF000BCB5B /* SettingsDAITAView.swift */, @@ -5987,6 +5990,7 @@ 588D7EDC2AF3A55E005DF40A /* ListAccessMethodInteractorProtocol.swift in Sources */, 588D7ED62AF3903F005DF40A /* ListAccessMethodViewController.swift in Sources */, 7A8A190E2CEB77C1000BCB5B /* SettingsRowViewFooter.swift in Sources */, + 7A3215742D3E5A85005DF395 /* DAITASettingsCoordinator.swift in Sources */, 7A6000FC2B628DF6001CF0D9 /* ListCellContentConfiguration.swift in Sources */, 582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */, 7A9FA1442A2E3FE5000B728D /* CheckableSettingsCell.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AppRoutes.swift b/ios/MullvadVPN/Classes/AppRoutes.swift index 53f74f0f3d..9f2d6a92b1 100644 --- a/ios/MullvadVPN/Classes/AppRoutes.swift +++ b/ios/MullvadVPN/Classes/AppRoutes.swift @@ -83,6 +83,11 @@ enum AppRoute: AppRouteProtocol { case settings(SettingsNavigationRoute?) /** + DAITA standalone route (not subsetting). + */ + case daita + + /** Select location route. */ case selectLocation @@ -130,7 +135,7 @@ enum AppRoute: AppRouteProtocol { return .selectLocation case .account: return .account - case .settings: + case .settings, .daita: return .settings case let .alert(id): return .alert(id) diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index 6c0c61b5a2..e2f2e6d32d 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -119,6 +119,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo case let .settings(subRoute): presentSettings(route: subRoute, animated: animated, completion: completion) + case .daita: + presentDAITA(animated: animated, completion: completion) + case .selectLocation: presentSelectLocation(animated: animated, completion: completion) @@ -597,6 +600,25 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo } } + private func presentDAITA(animated: Bool, completion: @escaping @Sendable (Coordinator) -> Void) { + let viewModel = DAITATunnelSettingsViewModel(tunnelManager: tunnelManager) + let coordinator = DAITASettingsCoordinator( + navigationController: CustomNavigationController(), + route: .daita, + viewModel: viewModel + ) + + coordinator.didFinish = { [weak self] _ in + self?.router.dismiss(.daita, animated: true) + } + + coordinator.start(animated: animated) + + presentChild(coordinator, animated: animated) { + completion(coordinator) + } + } + private func addTunnelObserver() { let tunnelObserver = TunnelBlockObserver( diff --git a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift index 8703c46e5d..e62defb9b4 100644 --- a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift @@ -230,7 +230,7 @@ extension LocationCoordinator: @preconcurrency LocationViewControllerWrapperDele } func navigateToDaitaSettings() { - applicationRouter?.present(.settings(nil)) + applicationRouter?.present(.daita) } func didSelectExitRelays(_ relays: UserSelectedRelays) { diff --git a/ios/MullvadVPN/Coordinators/Settings/DAITA/DAITASettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/DAITA/DAITASettingsCoordinator.swift new file mode 100644 index 0000000000..9ff06070ac --- /dev/null +++ b/ios/MullvadVPN/Coordinators/Settings/DAITA/DAITASettingsCoordinator.swift @@ -0,0 +1,124 @@ +// +// DAITASettingsCoordinator.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-01-20. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import Routing +import SwiftUI + +class DAITASettingsCoordinator: Coordinator, SettingsChildCoordinator, Presentable, Presenting { + private let navigationController: UINavigationController + private let viewModel: DAITATunnelSettingsViewModel + private var alertPresenter: AlertPresenter? + private let route: AppRoute + + var presentedViewController: UIViewController { + navigationController + } + + var didFinish: ((DAITASettingsCoordinator) -> Void)? + + init( + navigationController: UINavigationController, + route: AppRoute, + viewModel: DAITATunnelSettingsViewModel + ) { + self.navigationController = navigationController + self.route = route + self.viewModel = viewModel + + super.init() + + alertPresenter = AlertPresenter(context: self) + } + + func start(animated: Bool) { + let view = SettingsDAITAView(tunnelViewModel: self.viewModel) + + viewModel.didFailDAITAValidation = { [weak self] result in + guard let self else { return } + + showPrompt( + for: result.item, + onSave: { + self.viewModel.value = result.setting + }, + onDiscard: {} + ) + } + + let host = UIHostingController(rootView: view) + host.title = NSLocalizedString( + "NAVIGATION_TITLE_DAITA", + tableName: "Settings", + value: "DAITA", + comment: "" + ) + host.view.setAccessibilityIdentifier(.daitaView) + customiseNavigation(on: host) + + navigationController.pushViewController(host, animated: animated) + } + + private func customiseNavigation(on viewController: UIViewController) { + if route == .daita { + navigationController.navigationItem.largeTitleDisplayMode = .always + navigationController.navigationBar.prefersLargeTitles = true + + let doneButton = UIBarButtonItem( + systemItem: .done, + primaryAction: UIAction(handler: { [weak self] _ in + guard let self else { return } + didFinish?(self) + }) + ) + viewController.navigationItem.rightBarButtonItem = doneButton + } + } + + private func showPrompt( + for item: DAITASettingsPromptItem, + onSave: @escaping () -> Void, + onDiscard: @escaping () -> Void + ) { + let presentation = AlertPresentation( + id: "settings-daita-prompt", + accessibilityIdentifier: .daitaPromptAlert, + icon: .warning, + message: NSLocalizedString( + "SETTINGS_DAITA_ENABLE_TEXT", + tableName: "DAITA", + value: item.description, + comment: "" + ), + buttons: [ + AlertAction( + title: String(format: NSLocalizedString( + "SETTINGS_DAITA_ENABLE_OK_ACTION", + tableName: "DAITA", + value: "Enable \"%@\"", + comment: "" + ), item.title), + style: .default, + accessibilityId: .daitaConfirmAlertEnableButton, + handler: { onSave() } + ), + AlertAction( + title: NSLocalizedString( + "SETTINGS_DAITA_ENABLE_CANCEL_ACTION", + tableName: "DAITA", + value: "Cancel", + comment: "" + ), + style: .default, + handler: { onDiscard() } + ), + ] + ) + + alertPresenter?.showAlert(presentation: presentation, animated: true) + } +} diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift index f25a652d34..778900dec3 100644 --- a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift +++ b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift @@ -58,21 +58,21 @@ struct SettingsViewControllerFactory { // Handled separately and presented as a modal. .failed case .vpnSettings: - makeVPNSettingsViewController() + makeVPNSettingsViewCoordinator() case .problemReport: makeProblemReportViewController() case .apiAccess: - makeAPIAccessViewController() + makeAPIAccessCoordinator() case .changelog: - makeChangelogViewController() + makeChangelogCoordinator() case .multihop: makeMultihopViewController() case .daita: - makeDAITAViewController() + makeDAITASettingsCoordinator() } } - private func makeVPNSettingsViewController() -> MakeChildResult { + private func makeVPNSettingsViewCoordinator() -> MakeChildResult { return .childCoordinator(VPNSettingsCoordinator( navigationController: navigationController, interactorFactory: interactorFactory, @@ -87,7 +87,7 @@ struct SettingsViewControllerFactory { )) } - private func makeAPIAccessViewController() -> MakeChildResult { + private func makeAPIAccessCoordinator() -> MakeChildResult { return .childCoordinator(ListAccessMethodCoordinator( navigationController: navigationController, accessMethodRepository: accessMethodRepository, @@ -95,7 +95,7 @@ struct SettingsViewControllerFactory { )) } - private func makeChangelogViewController() -> MakeChildResult { + private func makeChangelogCoordinator() -> MakeChildResult { return .childCoordinator( ChangeLogCoordinator( navigationController: navigationController, @@ -120,72 +120,14 @@ struct SettingsViewControllerFactory { return .viewController(host) } - private func makeDAITAViewController() -> MakeChildResult { + private func makeDAITASettingsCoordinator() -> MakeChildResult { let viewModel = DAITATunnelSettingsViewModel(tunnelManager: interactorFactory.tunnelManager) - let view = SettingsDAITAView(tunnelViewModel: viewModel) - - viewModel.didFailDAITAValidation = { result in - showPrompt( - for: result.item, - onSave: { - viewModel.value = result.setting - }, - onDiscard: {} - ) - } - - let host = UIHostingController(rootView: view) - host.title = NSLocalizedString( - "NAVIGATION_TITLE_DAITA", - tableName: "Settings", - value: "DAITA", - comment: "" - ) - host.view.setAccessibilityIdentifier(.daitaView) - - return .viewController(host) - } - - private func showPrompt( - for item: DAITASettingsPromptItem, - onSave: @escaping () -> Void, - onDiscard: @escaping () -> Void - ) { - let presentation = AlertPresentation( - id: "settings-daita-prompt", - accessibilityIdentifier: .daitaPromptAlert, - icon: .warning, - message: NSLocalizedString( - "SETTINGS_DAITA_ENABLE_TEXT", - tableName: "DAITA", - value: item.description, - comment: "" - ), - buttons: [ - AlertAction( - title: String(format: NSLocalizedString( - "SETTINGS_DAITA_ENABLE_OK_ACTION", - tableName: "DAITA", - value: "Enable \"%@\"", - comment: "" - ), item.title), - style: .default, - accessibilityId: .daitaConfirmAlertEnableButton, - handler: { onSave() } - ), - AlertAction( - title: NSLocalizedString( - "SETTINGS_DAITA_ENABLE_CANCEL_ACTION", - tableName: "DAITA", - value: "Cancel", - comment: "" - ), - style: .default, - handler: { onDiscard() } - ), - ] + let coordinator = DAITASettingsCoordinator( + navigationController: navigationController, + route: .settings(.daita), + viewModel: viewModel ) - alertPresenter.showAlert(presentation: presentation, animated: true) + return .childCoordinator(coordinator) } } |
