diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index ccf1018d0526..0f414a07c159 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -135,7 +135,6 @@ 586A0DD12A20E371006C731C /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 586A0DD02A20E371006C731C /* WireGuardKitTypes */; }; 586A0DD42A20E4A9006C731C /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; }; 586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB122488ED2100095626 /* AlertPresenter.swift */; }; - 586A950D290125F0007BAF2B /* PresentAlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675D26E6839900655B05 /* PresentAlertOperation.swift */; }; 586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; }; 586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114028F841390037AF9A /* AddressCacheTracker.swift */; }; 586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */; }; @@ -390,12 +389,18 @@ 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; }; 7A02D4EB2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */; }; 7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; }; + 7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */; }; 7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */; }; 7A1A26432A2612AE00B978AA /* PaymentAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */; }; 7A21DACF2A30AA3700A787A9 /* UITextField+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.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 */; }; +<<<<<<< HEAD 7A3353972AAA0F8600F0A71C /* OperationBlockObserverSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */; }; +======= + 7A2960F62A963F7500389B82 /* AlertCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */; }; + 7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */; }; +>>>>>>> 010d1f888 (Coordinate alert presentation by integrating it into routing system) 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; }; 7A42DECD2A09064C00B209BE /* SelectableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */; }; 7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; }; @@ -435,7 +440,7 @@ 7ABCA5B72A9353C60044A708 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9F72983D36800BE19F7 /* Coordinator.swift */; }; 7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */; }; 7AE044BB2A935726003915D8 /* Routing.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A88DCD02A8FABBE00D2FF0E /* Routing.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 7AE47E522A17972A000418DA /* CustomAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE47E512A17972A000418DA /* CustomAlertViewController.swift */; }; + 7AE47E522A17972A000418DA /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE47E512A17972A000418DA /* AlertViewController.swift */; }; 7AF6E5F02A95051E00F2679D /* RouterBlockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */; }; 7AF6E5F12A95F4A500F2679D /* DurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FBFBF0291630700020E046 /* DurationTests.swift */; }; 7AF9BE992A4E0FE900DBFEDB /* MarkdownStylingOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE982A4E0FE900DBFEDB /* MarkdownStylingOptions.swift */; }; @@ -1034,7 +1039,6 @@ 581F23AC2A8CF92100788AB6 /* MockDefaultPathObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDefaultPathObserver.swift; sourceTree = ""; }; 581F23AE2A8CF94D00788AB6 /* MockPinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPinger.swift; sourceTree = ""; }; 5820675A26E6576800655B05 /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = ""; }; - 5820675D26E6839900655B05 /* PresentAlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAlertOperation.swift; sourceTree = ""; }; 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerErrors.swift; sourceTree = ""; }; 5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagementInteractor.swift; sourceTree = ""; }; 5820EDAA288FF0D2006BF4E4 /* DeviceRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRowView.swift; sourceTree = ""; }; @@ -1308,11 +1312,17 @@ 58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.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 = ""; }; + 7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coordinator+Router.swift"; sourceTree = ""; }; 7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentAlertPresenter.swift; sourceTree = ""; }; 7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+Appearance.swift"; sourceTree = ""; }; 7A307AD82A8CD8DA0017618B /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = ""; }; 7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extensions.swift"; sourceTree = ""; }; +<<<<<<< HEAD 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.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 = ""; }; +>>>>>>> 010d1f888 (Coordinate alert presentation by integrating it into routing system) 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = ""; }; 7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsCell.swift; sourceTree = ""; }; 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = ""; }; @@ -1344,7 +1354,7 @@ 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 = ""; }; 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = ""; }; - 7AE47E512A17972A000418DA /* CustomAlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertViewController.swift; sourceTree = ""; }; + 7AE47E512A17972A000418DA /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterBlockDelegate.swift; sourceTree = ""; }; 7AF9BE982A4E0FE900DBFEDB /* MarkdownStylingOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownStylingOptions.swift; sourceTree = ""; }; A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = ""; }; @@ -1771,10 +1781,11 @@ 583FE01629C196E8006E85F9 /* View controllers */ = { isa = PBXGroup; children = ( - F0EF50D12A8FA47E0031E8DF /* ChangeLog */, 583FE02029C1A0B1006E85F9 /* Account */, F0E8E4BF2A602C7D00ED26A3 /* AccountDeletion */, + 7A2960F72A964A3500389B82 /* Alert */, F0E8E4B92A55593300ED26A3 /* CreationAccount */, + F0EF50D12A8FA47E0031E8DF /* ChangeLog */, 583FE01D29C197C1006E85F9 /* DeviceList */, 583FE02529C1AD0E006E85F9 /* Launch */, 583FE02129C1A0F4006E85F9 /* Login */, @@ -1953,6 +1964,7 @@ 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */, 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */, 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */, + 7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */, 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */, 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */, 587D9675288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift */, @@ -2005,10 +2017,9 @@ children = ( 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */, F04FBE602A8379EE009278D7 /* AppPreferences.swift */, - 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */, 5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */, + 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */, 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */, - 7AE47E512A17972A000418DA /* CustomAlertViewController.swift */, 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */, 58138E60294871C600684F0C /* DeviceDataThrottling.swift */, 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */, @@ -2106,8 +2117,6 @@ 586A950B2901250A007BAF2B /* Operations */ = { isa = PBXGroup; children = ( - 58B9EB122488ED2100095626 /* AlertPresenter.swift */, - 5820675D26E6839900655B05 /* PresentAlertOperation.swift */, 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */, ); path = Operations; @@ -2297,6 +2306,7 @@ 7A9CCCAC2A96302800DD6A34 /* AccountDeletionCoordinator.swift */, 7A9CCCA82A96302700DD6A34 /* AccountRedeemingVoucherCoordinator.swift */, 7A9CCCA32A96302700DD6A34 /* AddCreditSucceededCoordinator.swift */, + 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */, 7A9CCCB12A96302800DD6A34 /* ApplicationCoordinator.swift */, 7A9CCCAA2A96302700DD6A34 /* ChangeLogCoordinator.swift */, 7A9CCCA92A96302700DD6A34 /* InAppPurchaseCoordinator.swift */, @@ -2548,6 +2558,16 @@ path = MullvadRESTTests; sourceTree = ""; }; + 7A2960F72A964A3500389B82 /* Alert */ = { + isa = PBXGroup; + children = ( + 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */, + 58B9EB122488ED2100095626 /* AlertPresenter.swift */, + 7AE47E512A17972A000418DA /* AlertViewController.swift */, + ); + path = Alert; + sourceTree = ""; + }; 7A83C3FC2A55B39500DFB83A /* TestPlans */ = { isa = PBXGroup; children = ( @@ -3784,6 +3804,7 @@ 7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */, 58C774BE29A7A249003A1A56 /* CustomNavigationController.swift in Sources */, E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */, + 7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */, 0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */, 58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */, 5864AF0929C78850005B0CD9 /* PreferencesCellFactory.swift in Sources */, @@ -3826,6 +3847,7 @@ 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */, 5864859929A0D028006C5743 /* FormsheetPresentationController.swift in Sources */, 7A9CCCB52A96302800DD6A34 /* AddCreditSucceededCoordinator.swift in Sources */, + 7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */, 58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */, 58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */, 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */, @@ -3868,7 +3890,7 @@ 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */, 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */, 5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */, - 7AE47E522A17972A000418DA /* CustomAlertViewController.swift in Sources */, + 7AE47E522A17972A000418DA /* AlertViewController.swift in Sources */, F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */, 58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */, 58CAFA002983FF0200BE19F7 /* LoginInteractor.swift in Sources */, @@ -3888,7 +3910,6 @@ 58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */, 58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */, 06410E07292D108E00AFC18C /* SettingsStore.swift in Sources */, - 586A950D290125F0007BAF2B /* PresentAlertOperation.swift in Sources */, 7A7AD28F29DEDB1C00480EF1 /* SettingsHeaderView.swift in Sources */, 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */, 58B26E262943522400D5980C /* NotificationProvider.swift in Sources */, @@ -3927,6 +3948,7 @@ 584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */, 584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */, 5875960A26F371FC00BF6711 /* Tunnel+Messaging.swift in Sources */, + 7A2960F62A963F7500389B82 /* AlertCoordinator.swift in Sources */, 063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */, 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */, 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */, diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 02691892fed1..000000000000 --- a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,22 +0,0 @@ -{ - "pins" : [ - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "173f567a2dfec11d74588eea82cecea555bdc0bc", - "version" : "1.4.0" - } - }, - { - "identity" : "wireguard-apple", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mullvad/wireguard-apple.git", - "state" : { - "revision" : "11a00c20dc03f2751db47e94f585c0778c7bde82" - } - } - ], - "version" : 2 -} diff --git a/ios/MullvadVPN/Classes/AppRoutes.swift b/ios/MullvadVPN/Classes/AppRoutes.swift index a217fe853921..cf81ddf4445d 100644 --- a/ios/MullvadVPN/Classes/AppRoutes.swift +++ b/ios/MullvadVPN/Classes/AppRoutes.swift @@ -13,7 +13,7 @@ import UIKit Enum type describing groups of routes. Each group is a modal layer with horizontal navigation inside with exception where primary navigation is a part of root controller on iPhone. */ -enum AppRouteGroup: AppRouteGroupProtocol { +enum AppRouteGroup: String, AppRouteGroupProtocol { /** Primary horizontal navigation group. */ @@ -39,12 +39,17 @@ enum AppRouteGroup: AppRouteGroupProtocol { */ case changelog + /** + Alert group. + */ + case alert + var isModal: Bool { switch self { case .primary: return UIDevice.current.userInterfaceIdiom == .pad - case .selectLocation, .account, .settings, .changelog: + case .selectLocation, .account, .settings, .changelog, .alert: return true } } @@ -55,6 +60,9 @@ enum AppRouteGroup: AppRouteGroupProtocol { return 0 case .settings, .account, .selectLocation, .changelog: return 1 + case .alert: + // Alerts should always be topmost. + return 999 } } } @@ -83,6 +91,11 @@ enum AppRoute: AppRouteProtocol { */ case changelog + /** + Alert route. + */ + case alert(AlertPresentation) + /** Routes that are part of primary horizontal navigation group. */ @@ -90,7 +103,7 @@ enum AppRoute: AppRouteProtocol { var isExclusive: Bool { switch self { - case .selectLocation, .account, .settings, .changelog: + case .selectLocation, .account, .settings, .changelog, .alert: return true default: return false @@ -117,6 +130,8 @@ enum AppRoute: AppRouteProtocol { return .account case .settings: return .settings + case .alert: + return .alert } } } diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift index 516ade62a10c..efc82800f8d7 100644 --- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift @@ -23,7 +23,6 @@ enum AddedMoreCreditOption: Equatable { final class AccountCoordinator: Coordinator, Presentable, Presenting { private let interactor: AccountInteractor private var accountController: AccountViewController? - private let alertPresenter = AlertPresenter() let navigationController: UINavigationController var presentedViewController: UIViewController { @@ -49,10 +48,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { let accountController = AccountViewController( interactor: interactor, - errorPresenter: PaymentAlertPresenter( - presentationController: presentationContext, - alertPresenter: alertPresenter - ) + errorPresenter: PaymentAlertPresenter(coordinator: self) ) accountController.actionHandler = handleViewControllerAction @@ -137,19 +133,18 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { // MARK: - Alerts private func logOut() { - let alertController = CustomAlertViewController(icon: .spinner) + let presentation = AlertPresentation(icon: .spinner, message: nil, buttons: []) - alertPresenter.enqueue(alertController, presentingController: presentationContext) { - self.interactor.logout { - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in - guard let self else { return } + interactor.logout { + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in + guard let self else { return } - alertController.dismiss(animated: true) { - self.didFinish?(self, .userLoggedOut) - } - } + applicationRouter?.dismiss(.alert(presentation), animated: true) + self.didFinish?(self, .userLoggedOut) } } + + applicationRouter?.present(.alert(presentation)) } private func showAccountDeviceInfo() { @@ -168,21 +163,19 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { comment: "" ) - let alertController = CustomAlertViewController( + let presentation = AlertPresentation( message: message, - icon: .info - ) - - alertController.addAction( - title: NSLocalizedString( - "DEVICE_INFO_DIALOG_OK_ACTION", - tableName: "Account", - value: "Got it!", - comment: "" - ), - style: .default + buttons: [AlertAction( + title: NSLocalizedString( + "DEVICE_INFO_DIALOG_OK_ACTION", + tableName: "Account", + value: "Got it!", + comment: "" + ), + style: .default + )] ) - alertPresenter.enqueue(alertController, presentingController: presentationContext) + applicationRouter?.present(.alert(presentation), animated: true) } } diff --git a/ios/MullvadVPN/Coordinators/AlertCoordinator.swift b/ios/MullvadVPN/Coordinators/AlertCoordinator.swift new file mode 100644 index 000000000000..3dbdc59f7aa5 --- /dev/null +++ b/ios/MullvadVPN/Coordinators/AlertCoordinator.swift @@ -0,0 +1,45 @@ +// +// AlertCoordinator.swift +// MullvadVPN +// +// Created by Jon Petersson on 2023-08-23. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import MullvadLogging +import Routing +import UIKit + +final class AlertCoordinator: Coordinator, Presentable { + private var alertController: AlertViewController? + private let presentation: AlertPresentation + + var didFinish: (() -> Void)? + + var presentedViewController: UIViewController { + return alertController! + } + + init(presentation: AlertPresentation) { + self.presentation = presentation + } + + func start() { + let alertController = AlertViewController( + header: presentation.header, + title: presentation.title, + message: presentation.message, + icon: presentation.icon + ) + + self.alertController = alertController + + alertController.onDismiss = { [weak self] in + self?.didFinish?() + } + + presentation.buttons.forEach { action in + alertController.addAction(title: action.title, style: action.style, handler: action.handler) + } + } +} diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index ae302ea87d36..e8787c453664 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -23,7 +23,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo /** Application router. */ - private var router: ApplicationRouter! + private(set) var router: ApplicationRouter! /** Primary navigation container. @@ -154,6 +154,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo case .welcome: presentWelcome(animated: animated, completion: completion) + + case let .alert(presentation): + presentAlert(presentation: presentation, animated: animated, completion: completion) } } @@ -162,15 +165,15 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo dismissWithContext context: RouteDismissalContext, completion: @escaping () -> Void ) { - if context.isClosing { - let dismissedRoute = context.dismissedRoutes.first! + let dismissedRoute = context.dismissedRoutes.first! + if context.isClosing { switch dismissedRoute.route.routeGroup { case .primary: endHorizontalFlow(animated: context.isAnimated, completion: completion) context.dismissedRoutes.forEach { $0.coordinator.removeFromParent() } - case .selectLocation, .account, .settings, .changelog: + case .selectLocation, .account, .settings, .changelog, .alert: guard let coordinator = dismissedRoute.coordinator as? Presentable else { completion() return assertionFailure("Expected presentable coordinator for \(dismissedRoute.route)") @@ -179,13 +182,13 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo coordinator.dismiss(animated: context.isAnimated, completion: completion) } } else { - let dismissedRoute = context.dismissedRoutes.first! assert(context.dismissedRoutes.count == 1) - if dismissedRoute.route == .outOfTime { - guard let coordinator = dismissedRoute.coordinator as? OutOfTimeCoordinator else { + switch dismissedRoute.route { + case .outOfTime, .welcome: + guard let coordinator = dismissedRoute.coordinator as? Poppable else { completion() - return assertionFailure("Unhandled coordinator for \(dismissedRoute.route)") + return assertionFailure("Expected presentable coordinator for \(dismissedRoute.route)") } coordinator.popFromNavigationStack( @@ -194,19 +197,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo ) coordinator.removeFromParent() - } else if dismissedRoute.route == .welcome { - guard let coordinator = dismissedRoute.coordinator as? WelcomeCoordinator else { - completion() - return assertionFailure("Unhandled coordinator for \(dismissedRoute.route)") - } - coordinator.popFromNavigationStack( - animated: context.isAnimated, - completion: completion - ) - - coordinator.removeFromParent() - } else { + default: assertionFailure("Unhandled dismissal for \(dismissedRoute.route)") completion() } @@ -649,6 +641,24 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } } + private func presentAlert( + presentation: AlertPresentation, + animated: Bool, + completion: @escaping (Coordinator) -> Void + ) { + let coordinator = AlertCoordinator(presentation: presentation) + + coordinator.didFinish = { [weak self] in + self?.router.dismiss(.alert(presentation)) + } + + coordinator.start() + + presentChild(coordinator, animated: animated) { + completion(coordinator) + } + } + private func makeTunnelCoordinator() -> TunnelCoordinator { let tunnelCoordinator = TunnelCoordinator(tunnelManager: tunnelManager) @@ -982,3 +992,10 @@ fileprivate extension AppPreferencesDataSource { // swiftlint:disable:next file_length } + +private protocol Poppable: Presentable { + func popFromNavigationStack( + animated: Bool, + completion: () -> Void + ) +} diff --git a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift index 3437b55f8bf6..ae86829b1211 100644 --- a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift @@ -11,7 +11,7 @@ import Routing import UIKit final class ChangeLogCoordinator: Coordinator, Presentable { - private var alertController: CustomAlertViewController? + private var alertController: AlertViewController? private let interactor: ChangeLogInteractor var didFinish: ((ChangeLogCoordinator) -> Void)? @@ -24,7 +24,7 @@ final class ChangeLogCoordinator: Coordinator, Presentable { } func start() { - let alertController = CustomAlertViewController( + let alertController = AlertViewController( header: interactor.viewModel.header, title: interactor.viewModel.title, attributedMessage: interactor.viewModel.body @@ -43,6 +43,7 @@ final class ChangeLogCoordinator: Coordinator, Presentable { didFinish?(self) } ) + self.alertController = alertController } } diff --git a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift index f4cea6a0138e..7d3a65df5bed 100644 --- a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift @@ -49,23 +49,27 @@ class InAppPurchaseCoordinator: Coordinator, Presentable { coordinator.start() case let .failure(failure): - let alertController = CustomAlertViewController( + let presentation = AlertPresentation( + icon: .alert, message: failure.error.localizedDescription, - icon: .alert + buttons: [ + AlertAction( + title: NSLocalizedString( + "IN_APP_PURCHASE_ERROR_DIALOG_OK_ACTION", + tableName: "Welcome", + value: "Got it!", + comment: "" + ), + style: .default, + handler: { [weak self] in + guard let self = self else { return } + self.didCancel?(self) + } + ), + ] ) - alertController.addAction( - title: NSLocalizedString( - "IN_APP_PURCHASE_ERROR_DIALOG_OK_ACTION", - tableName: "Welcome", - value: "Got it!", - comment: "" - ), - style: .default - ) - presentedViewController.present(alertController, animated: true) { - self.didCancel?(self) - } + applicationRouter?.present(.alert(presentation), animated: true) } } } diff --git a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift index 444913b059c0..af989dc628be 100644 --- a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift @@ -105,7 +105,10 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat accountNumber: accountNumber, devicesProxy: devicesProxy ) - let controller = DeviceManagementViewController(interactor: interactor) + let controller = DeviceManagementViewController( + interactor: interactor, + alertPresenter: AlertPresenter(coordinator: self) + ) controller.delegate = self controller.fetchDevices(animateUpdates: false) { [weak self] result in guard let self else { return } diff --git a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift index a001d2d060f9..137fbcf44665 100644 --- a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift @@ -42,10 +42,7 @@ class OutOfTimeCoordinator: Coordinator, OutOfTimeViewControllerDelegate { let controller = OutOfTimeViewController( interactor: interactor, - errorPresenter: PaymentAlertPresenter( - presentationController: navigationController, - alertPresenter: AlertPresenter() - ) + errorPresenter: PaymentAlertPresenter(coordinator: self) ) controller.delegate = self diff --git a/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift index 9e48af43ca28..1cd368fc7944 100644 --- a/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift @@ -158,12 +158,14 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV case .preferences: return PreferencesViewController( - interactor: interactorFactory.makePreferencesInteractor() + interactor: interactorFactory.makePreferencesInteractor(), + alertPresenter: AlertPresenter(coordinator: self) ) case .problemReport: return ProblemReportViewController( - interactor: interactorFactory.makeProblemReportInteractor() + interactor: interactorFactory.makeProblemReportInteractor(), + alertPresenter: AlertPresenter(coordinator: self) ) case .faq: diff --git a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift index da686aadca12..5f59a6e3bfd8 100644 --- a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift @@ -12,7 +12,6 @@ import UIKit class TunnelCoordinator: Coordinator { private let tunnelManager: TunnelManager private let controller: TunnelViewController - private let alertPresenter = AlertPresenter() private var tunnelObserver: TunnelObserver? @@ -59,40 +58,39 @@ class TunnelCoordinator: Coordinator { } private func showCancelTunnelAlert() { - let alertController = CustomAlertViewController( - title: nil, + let presentation = AlertPresentation( + icon: .alert, message: NSLocalizedString( "CANCEL_TUNNEL_ALERT_MESSAGE", tableName: "Main", value: "If you disconnect now, you won’t be able to secure your connection until the device is online.", comment: "" ), - icon: .alert + buttons: [ + AlertAction( + title: NSLocalizedString( + "CANCEL_TUNNEL_ALERT_DISCONNECT_ACTION", + tableName: "Main", + value: "Disconnect", + comment: "" + ), + style: .destructive, + handler: { [weak self] in + self?.tunnelManager.stopTunnel() + } + ), + AlertAction( + title: NSLocalizedString( + "CANCEL_TUNNEL_ALERT_CANCEL_ACTION", + tableName: "Main", + value: "Cancel", + comment: "" + ), + style: .default + ), + ] ) - alertController.addAction( - title: NSLocalizedString( - "CANCEL_TUNNEL_ALERT_DISCONNECT_ACTION", - tableName: "Main", - value: "Disconnect", - comment: "" - ), - style: .destructive, - handler: { [weak self] in - self?.tunnelManager.stopTunnel() - } - ) - - alertController.addAction( - title: NSLocalizedString( - "CANCEL_TUNNEL_ALERT_CANCEL_ACTION", - tableName: "Main", - value: "Cancel", - comment: "" - ), - style: .default - ) - - alertPresenter.enqueue(alertController, presentingController: rootViewController) + applicationRouter?.present(.alert(presentation), animated: true) } } diff --git a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift index de341fbce850..7185321f009d 100644 --- a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift @@ -84,36 +84,40 @@ final class WelcomeCoordinator: Coordinator, Presentable, Presenting { extension WelcomeCoordinator: WelcomeViewControllerDelegate { func didRequestToShowInfo(controller: WelcomeViewController) { - let message = """ - This is the name assigned to the device. Each device logged in on a \ - Mullvad account gets a unique name that helps \ - you identify it when you manage your devices in the app or on the website. - - You can have up to 5 devices logged in on one Mullvad account. - - If you log out, the device and the device name is removed. \ - When you log back in again, the device will get a new name. - """ - let alertController = CustomAlertViewController( - message: NSLocalizedString( - "WELCOME_DEVICE_CONCEPT_TEXT_DIALOG", - tableName: "Welcome", - value: message, - comment: "" - ), - icon: .info + let message = NSLocalizedString( + "WELCOME_DEVICE_CONCEPET_TEXT_DIALOG", + tableName: "Welcome", + value: + """ + This is the name assigned to the device. Each device logged in on a \ + Mullvad account gets a unique name that helps \ + you identify it when you manage your devices in the app or on the website. + + You can have up to 5 devices logged in on one Mullvad account. + + If you log out, the device and the device name is removed. \ + When you log back in again, the device will get a new name. + """, + comment: "" ) - alertController.addAction( - title: NSLocalizedString( - "WELCOME_DEVICE_NAME_DIALOG_OK_ACTION", - tableName: "Welcome", - value: "Got it!", - comment: "" - ), - style: .default + let presentation = AlertPresentation( + icon: .info, + message: message, + buttons: [ + AlertAction( + title: NSLocalizedString( + "WELCOME_DEVICE_NAME_DIALOG_OK_ACTION", + tableName: "Welcome", + value: "Got it!", + comment: "" + ), + style: .default + ), + ] ) - presentedViewController.present(alertController, animated: true) + + applicationRouter?.present(.alert(presentation), animated: true) } func didRequestToPurchaseCredit(controller: WelcomeViewController, accountNumber: String, product: SKProduct) { diff --git a/ios/MullvadVPN/Extensions/Coordinator+Router.swift b/ios/MullvadVPN/Extensions/Coordinator+Router.swift new file mode 100644 index 000000000000..8d9374a526d2 --- /dev/null +++ b/ios/MullvadVPN/Extensions/Coordinator+Router.swift @@ -0,0 +1,22 @@ +// +// Coordinator+Router.swift +// MullvadVPN +// +// Created by Jon Petersson on 2023-08-24. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Routing + +extension Coordinator { + var applicationRouter: ApplicationRouter? { + var appCoordinator: Coordinator? = self + + while appCoordinator?.parent != nil { + appCoordinator = appCoordinator?.parent + } + + return (appCoordinator as? ApplicationCoordinator)?.router + } +} diff --git a/ios/MullvadVPN/Operations/AlertPresenter.swift b/ios/MullvadVPN/Operations/AlertPresenter.swift deleted file mode 100644 index 7c31017a4e4b..000000000000 --- a/ios/MullvadVPN/Operations/AlertPresenter.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// AlertPresenter.swift -// MullvadVPN -// -// Created by pronebird on 04/06/2020. -// Copyright © 2020 Mullvad VPN AB. All rights reserved. -// - -import Operations -import UIKit - -final class AlertPresenter { - private let operationQueue = AsyncOperationQueue.makeSerial() - - func enqueue( - _ alertController: CustomAlertViewController, - presentingController: UIViewController, - presentCompletion: (() -> Void)? = nil - ) { - let operation = PresentAlertOperation( - alertController: alertController, - presentingController: presentingController, - presentCompletion: presentCompletion - ) - - operationQueue.addOperation(operation) - } - - func cancelAll() { - operationQueue.cancelAllOperations() - } -} diff --git a/ios/MullvadVPN/Operations/PresentAlertOperation.swift b/ios/MullvadVPN/Operations/PresentAlertOperation.swift index 7dff3399f6c2..cd6f5b02c2f4 100644 --- a/ios/MullvadVPN/Operations/PresentAlertOperation.swift +++ b/ios/MullvadVPN/Operations/PresentAlertOperation.swift @@ -10,12 +10,12 @@ import Operations import UIKit final class PresentAlertOperation: AsyncOperation { - private let alertController: CustomAlertViewController + private let alertController: AlertViewController private let presentingController: UIViewController private let presentCompletion: (() -> Void)? init( - alertController: CustomAlertViewController, + alertController: AlertViewController, presentingController: UIViewController, presentCompletion: (() -> Void)? = nil ) { diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index c1e9b91f9c4a..d07345c73e47 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -186,28 +186,26 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand // MARK: - SettingsMigrationUIHandler func showMigrationError(_ error: Error, completionHandler: @escaping () -> Void) { - let alertController = CustomAlertViewController( + let presentation = AlertPresentation( title: NSLocalizedString( "ALERT_TITLE", tableName: "SettingsMigrationUI", value: "Settings migration error", comment: "" ), - message: Self.migrationErrorReason(error) - ) - alertController.addAction( - title: NSLocalizedString("Got it!", tableName: "SettingsMigrationUI", comment: ""), - style: .default, - handler: { - completionHandler() - } + message: Self.migrationErrorReason(error), + buttons: [ + AlertAction( + title: NSLocalizedString("Got it!", tableName: "SettingsMigrationUI", comment: ""), + style: .default, + handler: { + completionHandler() + } + ), + ] ) - if let rootViewController = window?.rootViewController { - rootViewController.present(alertController, animated: true) - } else { - completionHandler() - } + appCoordinator?.router.present(.alert(presentation), animated: true) ?? completionHandler() } private static func migrationErrorReason(_ error: Error) -> String { diff --git a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift index 8d120a6a5b7b..8ca856688d16 100644 --- a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift +++ b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift @@ -7,36 +7,31 @@ // import MullvadREST -import UIKit +import Routing -class PaymentAlertPresenter { - private let presentationController: UIViewController - private let alertPresenter: AlertPresenter - - init(presentationController: UIViewController, alertPresenter: AlertPresenter) { - self.presentationController = presentationController - self.alertPresenter = alertPresenter - } +struct PaymentAlertPresenter { + let coordinator: Coordinator func showAlertForError( _ error: StorePaymentManagerError, context: REST.CreateApplePaymentResponse.Context, completion: (() -> Void)? = nil ) { - let alertController = CustomAlertViewController( + let presentation = AlertPresentation( title: context.errorTitle, - message: error.displayErrorDescription - ) - - alertController.addAction( - title: okButtonTextForKey("PAYMENT_ERROR_ALERT_OK_ACTION"), - style: .default, - handler: { - completion?() - } + message: error.displayErrorDescription, + buttons: [ + AlertAction( + title: okButtonTextForKey("PAYMENT_ERROR_ALERT_OK_ACTION"), + style: .default, + handler: { + completion?() + } + ), + ] ) - alertPresenter.enqueue(alertController, presentingController: presentationController) + coordinator.applicationRouter?.present(.alert(presentation), animated: true) } func showAlertForResponse( @@ -49,20 +44,21 @@ class PaymentAlertPresenter { return } - let alertController = CustomAlertViewController( + let presentation = AlertPresentation( title: response.alertTitle(context: context), - message: response.alertMessage(context: context) - ) - - alertController.addAction( - title: okButtonTextForKey("PAYMENT_RESPONSE_ALERT_OK_ACTION"), - style: .default, - handler: { - completion?() - } + message: response.alertMessage(context: context), + buttons: [ + AlertAction( + title: okButtonTextForKey("PAYMENT_RESPONSE_ALERT_OK_ACTION"), + style: .default, + handler: { + completion?() + } + ), + ] ) - alertPresenter.enqueue(alertController, presentingController: presentationController) + coordinator.applicationRouter?.present(.alert(presentation), animated: true) } private func okButtonTextForKey(_ key: String) -> String { diff --git a/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift b/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift new file mode 100644 index 000000000000..8a1993311096 --- /dev/null +++ b/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift @@ -0,0 +1,39 @@ +// +// AlertPresentation.swift +// MullvadVPN +// +// Created by Jon Petersson on 2023-08-23. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +struct AlertAction { + let title: String + let style: AlertActionStyle + var handler: (() -> Void)? +} + +struct AlertPresentation: Identifiable, CustomDebugStringConvertible { + let id = UUID() + + var header: String? + var icon: AlertIcon? + var title: String? + let message: String? + let buttons: [AlertAction] + + var debugDescription: String { + id.uuidString + } +} + +extension AlertPresentation: Equatable, Hashable { + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: AlertPresentation, rhs: AlertPresentation) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift b/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift new file mode 100644 index 000000000000..dc4dbd23b090 --- /dev/null +++ b/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift @@ -0,0 +1,17 @@ +// +// AlertPresenter.swift +// MullvadVPN +// +// Created by pronebird on 04/06/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Routing + +struct AlertPresenter { + let coordinator: Coordinator + + func showAlert(presentation: AlertPresentation, animated: Bool) { + coordinator.applicationRouter?.present(.alert(presentation), animated: animated) + } +} diff --git a/ios/MullvadVPN/Classes/CustomAlertViewController.swift b/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift similarity index 82% rename from ios/MullvadVPN/Classes/CustomAlertViewController.swift rename to ios/MullvadVPN/View controllers/Alert/AlertViewController.swift index cb6b92d1df2d..3241a0bd0164 100644 --- a/ios/MullvadVPN/Classes/CustomAlertViewController.swift +++ b/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift @@ -8,41 +8,40 @@ import UIKit -class CustomAlertViewController: UIViewController { - typealias Handler = () -> Void - - enum Icon { - case alert - case info - case spinner - - fileprivate var image: UIImage? { - switch self { - case .alert: - return UIImage(named: "IconAlert")?.withTintColor(.dangerColor) - case .info: - return UIImage(named: "IconInfo")?.withTintColor(.white) - default: - return nil - } +enum AlertActionStyle { + case `default` + case destructive + + fileprivate var buttonStyle: AppButton.Style { + switch self { + case .default: + return .default + case .destructive: + return .danger } } +} - enum ActionStyle { - case `default` - case destructive - - fileprivate var buttonStyle: AppButton.Style { - switch self { - case .default: - return .default - case .destructive: - return .danger - } +enum AlertIcon { + case alert + case info + case spinner + + fileprivate var image: UIImage? { + switch self { + case .alert: + return UIImage(named: "IconAlert")?.withTintColor(.dangerColor) + case .info: + return UIImage(named: "IconInfo")?.withTintColor(.white) + default: + return nil } } +} - var didDismiss: (() -> Void)? +class AlertViewController: UIViewController { + typealias Handler = () -> Void + var onDismiss: Handler? private let containerView: UIStackView = { let view = UIStackView() @@ -59,7 +58,7 @@ class CustomAlertViewController: UIViewController { private var handlers = [UIButton: Handler]() - init(header: String? = nil, title: String? = nil, message: String? = nil, icon: Icon? = nil) { + init(header: String? = nil, title: String? = nil, message: String? = nil, icon: AlertIcon? = nil) { super.init(nibName: nil, bundle: nil) setUp(header: header, title: title, icon: icon) { @@ -67,7 +66,7 @@ class CustomAlertViewController: UIViewController { } } - init(header: String? = nil, title: String? = nil, attributedMessage: NSAttributedString?, icon: Icon? = nil) { + init(header: String? = nil, title: String? = nil, attributedMessage: NSAttributedString?, icon: AlertIcon? = nil) { super.init(nibName: nil, bundle: nil) setUp(header: header, title: title, icon: icon) { @@ -80,7 +79,7 @@ class CustomAlertViewController: UIViewController { } // This code runs before viewDidLoad(). As such, no implicit calls to self.view should be made before this point. - private func setUp(header: String?, title: String?, icon: Icon?, addMessageCallback: () -> Void) { + private func setUp(header: String?, title: String?, icon: AlertIcon?, addMessageCallback: () -> Void) { modalPresentationStyle = .overFullScreen modalTransitionStyle = .crossDissolve @@ -121,7 +120,7 @@ class CustomAlertViewController: UIViewController { } } - func addAction(title: String, style: ActionStyle, handler: (() -> Void)? = nil) { + func addAction(title: String, style: AlertActionStyle, handler: (() -> Void)? = nil) { // The presence of a button should reset any custom button margin to default. containerView.directionalLayoutMargins.bottom = UIMetrics.CustomAlert.containerMargins.bottom @@ -191,12 +190,12 @@ class CustomAlertViewController: UIViewController { containerView.addArrangedSubview(label) } - private func addIcon(_ icon: Icon) { + private func addIcon(_ icon: AlertIcon) { let iconView = icon == .spinner ? getSpinnerView() : getImageView(for: icon) containerView.addArrangedSubview(iconView) } - private func getImageView(for icon: Icon) -> UIView { + private func getImageView(for icon: AlertIcon) -> UIView { let imageView = UIImageView() let imageContainerView = UIView() @@ -228,13 +227,11 @@ class CustomAlertViewController: UIViewController { } @objc private func didTapButton(_ button: AppButton) { - dismiss(animated: true) { [self] in - if let handler = handlers.removeValue(forKey: button) { - handler() - } - didDismiss?() - didDismiss = nil - handlers.removeAll() + if let handler = handlers.removeValue(forKey: button) { + handler() } + + handlers.removeAll() + onDismiss?() } } diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift index fa8e1f0945dc..e9f38ea54f1f 100644 --- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift +++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift @@ -32,8 +32,6 @@ class DeviceManagementViewController: UIViewController, RootContainment { .lightContent } - private let alertPresenter = AlertPresenter() - private let contentView: DeviceManagementContentView = { let contentView = DeviceManagementContentView() contentView.translatesAutoresizingMaskIntoConstraints = false @@ -42,9 +40,11 @@ class DeviceManagementViewController: UIViewController, RootContainment { private let logger = Logger(label: "DeviceManagementViewController") private let interactor: DeviceManagementInteractor + private let alertPresenter: AlertPresenter - init(interactor: DeviceManagementInteractor) { + init(interactor: DeviceManagementInteractor, alertPresenter: AlertPresenter) { self.interactor = interactor + self.alertPresenter = alertPresenter super.init(nibName: nil, bundle: nil) } @@ -157,69 +157,68 @@ class DeviceManagementViewController: UIViewController, RootContainment { } private func showErrorAlert(title: String, error: Error) { - let alertController = CustomAlertViewController( + let presentation = AlertPresentation( title: title, - message: getErrorDescription(error) - ) - - alertController.addAction( - title: NSLocalizedString( - "ERROR_ALERT_OK_ACTION", - tableName: "DeviceManagement", - value: "Got it!", - comment: "" - ), - style: .default + message: getErrorDescription(error), + buttons: [ + AlertAction( + title: NSLocalizedString( + "ERROR_ALERT_OK_ACTION", + tableName: "DeviceManagement", + value: "Got it!", + comment: "" + ), + style: .default + ), + ] ) - alertPresenter.enqueue(alertController, presentingController: self) + alertPresenter.showAlert(presentation: presentation, animated: true) } private func showLogoutConfirmation( deviceName: String, completion: @escaping (_ shouldDelete: Bool) -> Void ) { - let message = String( - format: NSLocalizedString( - "DELETE_ALERT_TITLE", - tableName: "DeviceManagement", - value: "Are you sure you want to log %@ out?", - comment: "" - ), deviceName - ) - - let alertController = CustomAlertViewController( - message: message, - icon: .alert - ) - - alertController.addAction( - title: NSLocalizedString( - "DELETE_ALERT_CANCEL_ACTION", - tableName: "DeviceManagement", - value: "Back", - comment: "" - ), - style: .default, - handler: { - completion(false) - } - ) - - alertController.addAction( - title: NSLocalizedString( - "DELETE_ALERT_CONFIRM_ACTION", - tableName: "DeviceManagement", - value: "Yes, log out device", - comment: "" + let presentation = AlertPresentation( + icon: .alert, + message: String( + format: NSLocalizedString( + "DELETE_ALERT_TITLE", + tableName: "DeviceManagement", + value: "Are you sure you want to log %@ out?", + comment: "" + ), deviceName ), - style: .destructive, - handler: { - completion(true) - } + buttons: [ + AlertAction( + title: NSLocalizedString( + "DELETE_ALERT_CANCEL_ACTION", + tableName: "DeviceManagement", + value: "Back", + comment: "" + ), + style: .default, + handler: { + completion(false) + } + ), + AlertAction( + title: NSLocalizedString( + "DELETE_ALERT_CONFIRM_ACTION", + tableName: "DeviceManagement", + value: "Yes, log out device", + comment: "" + ), + style: .destructive, + handler: { + completion(true) + } + ), + ] ) - alertPresenter.enqueue(alertController, presentingController: self) + alertPresenter.showAlert(presentation: presentation, animated: true) } private func deleteDevice(identifier: String, completionHandler: @escaping (Error?) -> Void) { diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift index ef05999fe4bb..83ab9afb0a14 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift @@ -11,14 +11,16 @@ import UIKit class PreferencesViewController: UITableViewController, PreferencesDataSourceDelegate { private let interactor: PreferencesInteractor private var dataSource: PreferencesDataSource? - private let alertPresenter = AlertPresenter() + private let alertPresenter: AlertPresenter override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } - init(interactor: PreferencesInteractor) { + init(interactor: PreferencesInteractor, alertPresenter: AlertPresenter) { self.interactor = interactor + self.alertPresenter = alertPresenter + super.init(style: .grouped) } @@ -76,22 +78,23 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel } private func showContentBlockerInfo(with message: String) { - let alertController = CustomAlertViewController( + let presentation = AlertPresentation( + icon: .info, message: message, - icon: .info - ) - - alertController.addAction( - title: NSLocalizedString( - "PREFERENCES_CONTENT_BLOCKERS_OK_ACTION", - tableName: "ContentBlockers", - value: "Got it!", - comment: "" - ), - style: .default + buttons: [ + AlertAction( + title: NSLocalizedString( + "PREFERENCES_CONTENT_BLOCKERS_OK_ACTION", + tableName: "ContentBlockers", + value: "Got it!", + comment: "" + ), + style: .default + ), + ] ) - alertPresenter.enqueue(alertController, presentingController: self) + alertPresenter.showAlert(presentation: presentation, animated: true) } private func humanReadablePortRepresentation(_ ranges: [[UInt16]]) -> String { diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift index 20b29e336425..b851cf828779 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift @@ -13,6 +13,7 @@ import UIKit final class ProblemReportViewController: UIViewController, UITextFieldDelegate { private let interactor: ProblemReportInteractor + private let alertPresenter: AlertPresenter private var textViewKeyboardResponder: AutomaticKeyboardResponder? private var scrollViewKeyboardResponder: AutomaticKeyboardResponder? @@ -193,8 +194,9 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { false } - init(interactor: ProblemReportInteractor) { + init(interactor: ProblemReportInteractor, alertPresenter: AlertPresenter) { self.interactor = interactor + self.alertPresenter = alertPresenter super.init(nibName: nil, bundle: nil) } @@ -507,48 +509,46 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { } private func presentEmptyEmailConfirmationAlert(completion: @escaping (Bool) -> Void) { - let message = NSLocalizedString( - "EMPTY_EMAIL_ALERT_MESSAGE", - tableName: "ProblemReport", - value: """ - You are about to send the problem report without a way for us to get back to you. \ - If you want an answer to your report you will have to enter an email address. - """, - comment: "" - ) - - let alertController = CustomAlertViewController( - message: message, - icon: .alert - ) - - alertController.addAction( - title: NSLocalizedString( - "EMPTY_EMAIL_ALERT_SEND_ANYWAY_ACTION", + let presentation = AlertPresentation( + icon: .alert, + message: NSLocalizedString( + "EMPTY_EMAIL_ALERT_MESSAGE", tableName: "ProblemReport", - value: "Send anyway", + value: """ + You are about to send the problem report without a way for us to get back to you. \ + If you want an answer to your report you will have to enter an email address. + """, comment: "" ), - style: .destructive, - handler: { - completion(true) - } - ) - - alertController.addAction( - title: NSLocalizedString( - "EMPTY_EMAIL_ALERT_CANCEL_ACTION", - tableName: "ProblemReport", - value: "Cancel", - comment: "" - ), - style: .default, - handler: { - completion(false) - } + buttons: [ + AlertAction( + title: NSLocalizedString( + "EMPTY_EMAIL_ALERT_SEND_ANYWAY_ACTION", + tableName: "ProblemReport", + value: "Send anyway", + comment: "" + ), + style: .destructive, + handler: { + completion(true) + } + ), + AlertAction( + title: NSLocalizedString( + "EMPTY_EMAIL_ALERT_CANCEL_ACTION", + tableName: "ProblemReport", + value: "Cancel", + comment: "" + ), + style: .default, + handler: { + completion(false) + } + ), + ] ) - present(alertController, animated: true) + alertPresenter.showAlert(presentation: presentation, animated: true) } // MARK: - Private: Problem report submission diff --git a/ios/Routing/Coordinator.swift b/ios/Routing/Coordinator.swift index 217a951aef7a..c111b76ee58a 100644 --- a/ios/Routing/Coordinator.swift +++ b/ios/Routing/Coordinator.swift @@ -84,7 +84,7 @@ open class Coordinator: NSObject { */ public protocol Presentable: Coordinator { /** - View controller that is presented modally. It's expected it to be the top-most view controller + View controller that is presented modally. It's expected it to be the topmost view controller managed by coordinator. */ var presentedViewController: UIViewController { get } diff --git a/ios/Routing/Router/ApplicationRouter.swift b/ios/Routing/Router/ApplicationRouter.swift index 2de0cbc3ff23..957398f77451 100644 --- a/ios/Routing/Router/ApplicationRouter.swift +++ b/ios/Routing/Router/ApplicationRouter.swift @@ -81,7 +81,7 @@ public final class ApplicationRouter { } private func enqueue(_ pendingRoute: PendingRoute) { - logger.debug("Enqueue \(pendingRoute.operation).") + logger.debug("\(pendingRoute.operation).") pendingRoutes.append(pendingRoute) diff --git a/ios/Routing/Router/ApplicationRouterTypes.swift b/ios/Routing/Router/ApplicationRouterTypes.swift index 1dfd128c0a2e..78bef90da48c 100644 --- a/ios/Routing/Router/ApplicationRouterTypes.swift +++ b/ios/Routing/Router/ApplicationRouterTypes.swift @@ -69,7 +69,7 @@ enum PendingDismissalResult { /** Enum describing operation over the route. */ -enum RouteOperation: Equatable { +enum RouteOperation: Equatable, CustomDebugStringConvertible { /** Present route. */ @@ -91,12 +91,23 @@ enum RouteOperation: Equatable { return dismissMatch.routeGroup } } + + var debugDescription: String { + let action: String + switch self { + case let .present(routeType): + action = "Presenting .\(routeType)" + case let .dismiss(match): + action = "Dismissing .\(match)" + } + return "\(action)" + } } /** Enum type describing a single route or a group of routes requested to be dismissed. */ -enum DismissMatch: Equatable { +enum DismissMatch: Equatable, CustomDebugStringConvertible { case group(RouteType.RouteGroupType) case singleRoute(RouteType) @@ -111,6 +122,15 @@ enum DismissMatch: Equatable { return route.routeGroup } } + + var debugDescription: String { + switch self { + case let .group(group): + return "\(group)" + case let .singleRoute(route): + return "\(route)" + } + } } /**