summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2023-09-13 16:28:16 +0200
committerBug Magnet <marco.nikic@mullvad.net>2023-09-13 16:28:16 +0200
commit571d6bd61dfb7151ca28ebd4cce0516f710b7c9f (patch)
treed01938a194efeac8bebf10616b31dbd23bb47f04
parentc0ee7f5b8686a4ed67cec01b574a43c7e9ef0287 (diff)
parentf47bbebe41337e471a602f969a05dd06e8904779 (diff)
downloadmullvadvpn-571d6bd61dfb7151ca28ebd4cce0516f710b7c9f.tar.xz
mullvadvpn-571d6bd61dfb7151ca28ebd4cce0516f710b7c9f.zip
Merge branch 'coordinate-alert-presentation-ios-175'
-rw-r--r--ios/.swiftlint.yml2
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj38
-rw-r--r--ios/MullvadVPN/Classes/AppRoutes.swift20
-rw-r--r--ios/MullvadVPN/Coordinators/AccountCoordinator.swift60
-rw-r--r--ios/MullvadVPN/Coordinators/AlertCoordinator.swift45
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift70
-rw-r--r--ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift5
-rw-r--r--ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift36
-rw-r--r--ios/MullvadVPN/Coordinators/LoginCoordinator.swift13
-rw-r--r--ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift15
-rw-r--r--ios/MullvadVPN/Coordinators/SettingsCoordinator.swift10
-rw-r--r--ios/MullvadVPN/Coordinators/TunnelCoordinator.swift62
-rw-r--r--ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift68
-rw-r--r--ios/MullvadVPN/Extensions/Coordinator+Router.swift22
-rw-r--r--ios/MullvadVPN/Operations/AlertPresenter.swift32
-rw-r--r--ios/MullvadVPN/Operations/PresentAlertOperation.swift59
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift33
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift4
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift5
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift2
-rw-r--r--ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift62
-rw-r--r--ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift45
-rw-r--r--ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift31
-rw-r--r--ios/MullvadVPN/View controllers/Alert/AlertViewController.swift (renamed from ios/MullvadVPN/Classes/CustomAlertViewController.swift)79
-rw-r--r--ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift117
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift37
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift86
-rw-r--r--ios/Operations/AsyncOperation.swift3
-rw-r--r--ios/Routing/Coordinator.swift33
-rw-r--r--ios/Routing/Router/AppRouteProtocol.swift4
-rw-r--r--ios/Routing/Router/ApplicationRouter.swift27
-rw-r--r--ios/Routing/Router/ApplicationRouterDelegate.swift2
-rw-r--r--ios/Routing/Router/ApplicationRouterTypes.swift53
-rw-r--r--ios/RoutingTests/RouterBlockDelegate.swift6
34 files changed, 704 insertions, 482 deletions
diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml
index 9e7704961d..5a66346daa 100644
--- a/ios/.swiftlint.yml
+++ b/ios/.swiftlint.yml
@@ -31,6 +31,8 @@ line_length:
ignores_interpolated_strings: true
warning: 120
error: 300
+cyclomatic_complexity:
+ ignores_case_statements: true
type_name:
min_length: 4
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 7aab178ca5..f65ffd2851 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,6 +389,7 @@
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 */; };
@@ -399,6 +399,8 @@
7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */; };
7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; };
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 */; };
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 */; };
@@ -438,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 */; };
@@ -1040,7 +1042,6 @@
581F23AC2A8CF92100788AB6 /* MockDefaultPathObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDefaultPathObserver.swift; sourceTree = "<group>"; };
581F23AE2A8CF94D00788AB6 /* MockPinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPinger.swift; sourceTree = "<group>"; };
5820675A26E6576800655B05 /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; };
- 5820675D26E6839900655B05 /* PresentAlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAlertOperation.swift; sourceTree = "<group>"; };
5820676326E771DB00655B05 /* TunnelManagerErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerErrors.swift; sourceTree = "<group>"; };
5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagementInteractor.swift; sourceTree = "<group>"; };
5820EDAA288FF0D2006BF4E4 /* DeviceRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRowView.swift; sourceTree = "<group>"; };
@@ -1314,6 +1315,7 @@
58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNScreenshots.xctestplan; sourceTree = "<group>"; };
7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = "<group>"; };
+ 7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coordinator+Router.swift"; sourceTree = "<group>"; };
7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentAlertPresenter.swift; sourceTree = "<group>"; };
7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+Appearance.swift"; sourceTree = "<group>"; };
7A307AD82A8CD8DA0017618B /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = "<group>"; };
@@ -1322,6 +1324,8 @@
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>"; };
7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = "<group>"; };
+ 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCoordinator.swift; sourceTree = "<group>"; };
+ 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresentation.swift; sourceTree = "<group>"; };
7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = "<group>"; };
7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsCell.swift; sourceTree = "<group>"; };
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; };
@@ -1353,7 +1357,7 @@
7A9CCCB12A96302800DD6A34 /* ApplicationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationCoordinator.swift; sourceTree = "<group>"; };
7A9CCCB22A96302800DD6A34 /* TunnelCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelCoordinator.swift; sourceTree = "<group>"; };
7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = "<group>"; };
- 7AE47E512A17972A000418DA /* CustomAlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertViewController.swift; sourceTree = "<group>"; };
+ 7AE47E512A17972A000418DA /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = "<group>"; };
7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterBlockDelegate.swift; sourceTree = "<group>"; };
7AF9BE982A4E0FE900DBFEDB /* MarkdownStylingOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownStylingOptions.swift; sourceTree = "<group>"; };
A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = "<group>"; };
@@ -1784,10 +1788,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 */,
@@ -1966,6 +1971,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 */,
@@ -2021,10 +2027,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 */,
@@ -2122,8 +2127,6 @@
586A950B2901250A007BAF2B /* Operations */ = {
isa = PBXGroup;
children = (
- 58B9EB122488ED2100095626 /* AlertPresenter.swift */,
- 5820675D26E6839900655B05 /* PresentAlertOperation.swift */,
5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */,
);
path = Operations;
@@ -2312,6 +2315,7 @@
7A9CCCAF2A96302800DD6A34 /* AccountCoordinator.swift */,
7A9CCCAC2A96302800DD6A34 /* AccountDeletionCoordinator.swift */,
7A9CCCA32A96302700DD6A34 /* AddCreditSucceededCoordinator.swift */,
+ 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */,
7A9CCCB12A96302800DD6A34 /* ApplicationCoordinator.swift */,
7A9CCCAA2A96302700DD6A34 /* ChangeLogCoordinator.swift */,
7A9CCCA82A96302700DD6A34 /* CreateAccountVoucherCoordinator.swift */,
@@ -2564,6 +2568,16 @@
path = MullvadRESTTests;
sourceTree = "<group>";
};
+ 7A2960F72A964A3500389B82 /* Alert */ = {
+ isa = PBXGroup;
+ children = (
+ 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */,
+ 58B9EB122488ED2100095626 /* AlertPresenter.swift */,
+ 7AE47E512A17972A000418DA /* AlertViewController.swift */,
+ );
+ path = Alert;
+ sourceTree = "<group>";
+ };
7A83C3FC2A55B39500DFB83A /* TestPlans */ = {
isa = PBXGroup;
children = (
@@ -3804,6 +3818,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 */,
@@ -3846,6 +3861,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 */,
@@ -3890,7 +3906,7 @@
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */,
5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */,
7A3353912AAA014400F0A71C /* SimulatorVPNConnection.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 */,
@@ -3910,7 +3926,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 */,
@@ -3949,6 +3964,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/Classes/AppRoutes.swift b/ios/MullvadVPN/Classes/AppRoutes.swift
index a217fe8539..13c087b9e5 100644
--- a/ios/MullvadVPN/Classes/AppRoutes.swift
+++ b/ios/MullvadVPN/Classes/AppRoutes.swift
@@ -39,12 +39,17 @@ enum AppRouteGroup: AppRouteGroupProtocol {
*/
case changelog
+ /**
+ Alert group. Alert id should match the id of the alert being contained.
+ */
+ case alert(_ alertId: String)
+
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 .max
}
}
}
@@ -84,13 +92,19 @@ enum AppRoute: AppRouteProtocol {
case changelog
/**
+ Alert route. Alert id must be a unique string in order to produce a unique route
+ that distinguishes between different kinds of alerts.
+ */
+ case alert(_ alertId: String)
+
+ /**
Routes that are part of primary horizontal navigation group.
*/
case tos, login, main, revoked, outOfTime, welcome
var isExclusive: Bool {
switch self {
- case .selectLocation, .account, .settings, .changelog:
+ case .selectLocation, .account, .settings, .changelog, .alert:
return true
default:
return false
@@ -117,6 +131,8 @@ enum AppRoute: AppRouteProtocol {
return .account
case .settings:
return .settings
+ case let .alert(id):
+ return .alert(id)
}
}
}
diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift
index b1811001c2..0338d7427b 100644
--- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift
@@ -23,17 +23,12 @@ 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 {
navigationController
}
- var presentationContext: UIViewController {
- navigationController
- }
-
var didFinish: ((AccountCoordinator, AccountDismissReason) -> Void)?
init(
@@ -49,10 +44,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
let accountController = AccountViewController(
interactor: interactor,
- errorPresenter: PaymentAlertPresenter(
- presentationController: presentationContext,
- alertPresenter: alertPresenter
- )
+ errorPresenter: PaymentAlertPresenter(alertContext: self)
)
accountController.actionHandler = handleViewControllerAction
@@ -141,19 +133,25 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
// MARK: - Alerts
private func logOut() {
- let alertController = CustomAlertViewController(icon: .spinner)
+ let presentation = AlertPresentation(
+ id: "account-logout-alert",
+ 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 }
+ let alertPresenter = AlertPresenter(context: self)
- alertController.dismiss(animated: true) {
- self.didFinish?(self, .userLoggedOut)
- }
- }
+ interactor.logout {
+ DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in
+ guard let self else { return }
+
+ alertPresenter.dismissAlert(presentation: presentation, animated: true)
+ self.didFinish?(self, .userLoggedOut)
}
}
+
+ alertPresenter.showAlert(presentation: presentation, animated: true)
}
private func showAccountDeviceInfo() {
@@ -172,21 +170,21 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
comment: ""
)
- let alertController = CustomAlertViewController(
+ let presentation = AlertPresentation(
+ id: "account-device-info-alert",
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)
+ let presenter = AlertPresenter(context: self)
+ presenter.showAlert(presentation: presentation, animated: true)
}
}
diff --git a/ios/MullvadVPN/Coordinators/AlertCoordinator.swift b/ios/MullvadVPN/Coordinators/AlertCoordinator.swift
new file mode 100644
index 0000000000..3dbdc59f7a
--- /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 bcc87d657a..7f6cb1d474 100644
--- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
@@ -17,14 +17,13 @@ import UIKit
Application coordinator managing split view and two navigation contexts.
*/
final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewControllerDelegate,
- UISplitViewControllerDelegate, ApplicationRouterDelegate,
- NotificationManagerDelegate {
+ UISplitViewControllerDelegate, ApplicationRouterDelegate, NotificationManagerDelegate {
typealias RouteType = AppRoute
/**
Application router.
*/
- private var router: ApplicationRouter<AppRoute>!
+ private(set) var router: ApplicationRouter<AppRoute>!
/**
Primary navigation container.
@@ -127,11 +126,11 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
func applicationRouter(
_ router: ApplicationRouter<RouteType>,
- route: AppRoute,
+ presentWithContext context: RoutePresentationContext<RouteType>,
animated: Bool,
completion: @escaping (Coordinator) -> Void
) {
- switch route {
+ switch context.route {
case .account:
presentAccount(animated: animated, completion: completion)
@@ -161,6 +160,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
case .welcome:
presentWelcome(animated: animated, completion: completion)
+
+ case .alert:
+ presentAlert(animated: animated, context: context, completion: completion)
}
}
@@ -169,15 +171,15 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
dismissWithContext context: RouteDismissalContext<RouteType>,
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)")
@@ -186,13 +188,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(
@@ -201,19 +203,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()
}
@@ -579,11 +570,10 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
)
coordinator.didFinishPayment = { [weak self] _ in
- guard let self else { return }
+ guard let self = self else { return }
if shouldDismissOutOfTime() {
router.dismiss(.outOfTime, animated: true)
-
continueFlow(animated: true)
}
}
@@ -628,10 +618,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
!(tunnelManager.deviceState.accountData?.isExpired ?? false)
}
- private func presentSelectLocation(
- animated: Bool,
- completion: @escaping (Coordinator) -> Void
- ) {
+ private func presentSelectLocation(animated: Bool, completion: @escaping (Coordinator) -> Void) {
let coordinator = makeSelectLocationCoordinator(forModalPresentation: true)
coordinator.start()
@@ -664,6 +651,29 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
}
}
+ private func presentAlert(
+ animated: Bool,
+ context: RoutePresentationContext<RouteType>,
+ completion: @escaping (Coordinator) -> Void
+ ) {
+ guard let metadata = context.metadata as? AlertMetadata else {
+ assertionFailure("Could not get AlertMetadata from RoutePresentationContext.")
+ return
+ }
+
+ let coordinator = AlertCoordinator(presentation: metadata.presentation)
+
+ coordinator.didFinish = { [weak self] in
+ self?.router.dismiss(context.route)
+ }
+
+ coordinator.start()
+
+ metadata.context.presentChild(coordinator, animated: animated) {
+ completion(coordinator)
+ }
+ }
+
private func makeTunnelCoordinator() -> TunnelCoordinator {
let tunnelCoordinator = TunnelCoordinator(tunnelManager: tunnelManager)
diff --git a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift
index 3437b55f8b..ae86829b12 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 f4cea6a013..e2ccb578df 100644
--- a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift
@@ -11,7 +11,7 @@ import Routing
import StoreKit
import UIKit
-class InAppPurchaseCoordinator: Coordinator, Presentable {
+class InAppPurchaseCoordinator: Coordinator, Presenting, Presentable {
private let navigationController: RootContainerViewController
private let interactor: InAppPurchaseInteractor
@@ -49,23 +49,29 @@ class InAppPurchaseCoordinator: Coordinator, Presentable {
coordinator.start()
case let .failure(failure):
- let alertController = CustomAlertViewController(
+ let presentation = AlertPresentation(
+ id: "in-app-purchase-error-alert",
+ 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)
- }
+ let presenter = AlertPresenter(context: self)
+ presenter.showAlert(presentation: presentation, animated: true)
}
}
}
diff --git a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift
index a85353c410..f62869f271 100644
--- a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift
@@ -13,7 +13,7 @@ import Operations
import Routing
import UIKit
-final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegate {
+final class LoginCoordinator: Coordinator, Presenting, DeviceManagementViewControllerDelegate {
private let tunnelManager: TunnelManager
private let devicesProxy: REST.DevicesProxy
@@ -25,6 +25,9 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat
var didCreateAccount: (() -> Void)?
var preferredAccountNumberPublisher: AnyPublisher<String, Never>?
+ var presentationContext: UIViewController {
+ navigationController
+ }
let navigationController: RootContainerViewController
@@ -117,10 +120,14 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat
accountNumber: accountNumber,
devicesProxy: devicesProxy
)
- let controller = DeviceManagementViewController(interactor: interactor)
+ let controller = DeviceManagementViewController(
+ interactor: interactor,
+ alertPresenter: AlertPresenter(context: self)
+ )
controller.delegate = self
+
controller.fetchDevices(animateUpdates: false) { [weak self] result in
- guard let self else { return }
+ guard let self = self else { return }
switch result {
case .success:
diff --git a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift
index a001d2d060..5c333c4564 100644
--- a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift
@@ -9,13 +9,17 @@
import Routing
import UIKit
-class OutOfTimeCoordinator: Coordinator, OutOfTimeViewControllerDelegate {
+class OutOfTimeCoordinator: Coordinator, Presenting, OutOfTimeViewControllerDelegate, Poppable {
let navigationController: RootContainerViewController
let storePaymentManager: StorePaymentManager
let tunnelManager: TunnelManager
var didFinishPayment: ((OutOfTimeCoordinator) -> Void)?
+ var presentedViewController: UIViewController {
+ navigationController
+ }
+
private(set) var isMakingPayment = false
private var viewController: OutOfTimeViewController?
@@ -42,10 +46,7 @@ class OutOfTimeCoordinator: Coordinator, OutOfTimeViewControllerDelegate {
let controller = OutOfTimeViewController(
interactor: interactor,
- errorPresenter: PaymentAlertPresenter(
- presentationController: navigationController,
- alertPresenter: AlertPresenter()
- )
+ errorPresenter: PaymentAlertPresenter(alertContext: self)
)
controller.delegate = self
@@ -55,9 +56,9 @@ class OutOfTimeCoordinator: Coordinator, OutOfTimeViewControllerDelegate {
navigationController.pushViewController(controller, animated: animated)
}
- func popFromNavigationStack(animated: Bool, completion: @escaping () -> Void) {
+ func popFromNavigationStack(animated: Bool, completion: (() -> Void)?) {
guard let viewController else {
- completion()
+ completion?()
return
}
diff --git a/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift
index 9e48af43ca..88a32826b5 100644
--- a/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift
@@ -32,10 +32,6 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV
navigationController
}
- var presentationContext: UIViewController {
- navigationController
- }
-
var willNavigate: ((
_ coordinator: SettingsCoordinator,
_ from: SettingsNavigationRoute?,
@@ -158,12 +154,14 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV
case .preferences:
return PreferencesViewController(
- interactor: interactorFactory.makePreferencesInteractor()
+ interactor: interactorFactory.makePreferencesInteractor(),
+ alertPresenter: AlertPresenter(context: self)
)
case .problemReport:
return ProblemReportViewController(
- interactor: interactorFactory.makeProblemReportInteractor()
+ interactor: interactorFactory.makeProblemReportInteractor(),
+ alertPresenter: AlertPresenter(context: self)
)
case .faq:
diff --git a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
index da686aadca..255728ccef 100644
--- a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
@@ -9,13 +9,16 @@
import Routing
import UIKit
-class TunnelCoordinator: Coordinator {
+class TunnelCoordinator: Coordinator, Presenting {
private let tunnelManager: TunnelManager
private let controller: TunnelViewController
- private let alertPresenter = AlertPresenter()
private var tunnelObserver: TunnelObserver?
+ var presentationContext: UIViewController {
+ controller
+ }
+
var rootViewController: UIViewController {
controller
}
@@ -59,40 +62,41 @@ class TunnelCoordinator: Coordinator {
}
private func showCancelTunnelAlert() {
- let alertController = CustomAlertViewController(
- title: nil,
+ let presentation = AlertPresentation(
+ id: "main-cancel-tunnel-alert",
+ 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
- )
-
- 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
+ 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
+ ),
+ ]
)
- alertPresenter.enqueue(alertController, presentingController: rootViewController)
+ let presenter = AlertPresenter(context: self)
+ presenter.showAlert(presentation: presentation, animated: true)
}
}
diff --git a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift
index 7a489478df..a614dd780f 100644
--- a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift
@@ -12,7 +12,7 @@ import Routing
import StoreKit
import UIKit
-final class WelcomeCoordinator: Coordinator, Presentable, Presenting {
+final class WelcomeCoordinator: Coordinator, Poppable, Presenting {
private let navigationController: RootContainerViewController
private let storePaymentManager: StorePaymentManager
private let tunnelManager: TunnelManager
@@ -28,10 +28,6 @@ final class WelcomeCoordinator: Coordinator, Presentable, Presenting {
navigationController
}
- var presentationContext: UIViewController {
- navigationController
- }
-
init(
navigationController: RootContainerViewController,
storePaymentManager: StorePaymentManager,
@@ -70,11 +66,11 @@ final class WelcomeCoordinator: Coordinator, Presentable, Presenting {
navigationController.pushViewController(controller, animated: animated)
}
- func popFromNavigationStack(animated: Bool, completion: @escaping () -> Void) {
+ func popFromNavigationStack(animated: Bool, completion: (() -> Void)?) {
guard let viewController,
let index = navigationController.viewControllers.firstIndex(of: viewController)
else {
- completion()
+ completion?()
return
}
navigationController.setViewControllers(
@@ -87,36 +83,42 @@ 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.
+ let message = NSLocalizedString(
+ "WELCOME_DEVICE_CONCEPT_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.
+ 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
+ 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(
+ id: "welcome-device-name-alert",
+ 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)
+
+ let presenter = AlertPresenter(context: self)
+ presenter.showAlert(presentation: presentation, animated: true)
}
func didRequestToPurchaseCredit(controller: WelcomeViewController, accountNumber: String, product: SKProduct) {
@@ -153,7 +155,7 @@ extension WelcomeCoordinator: WelcomeViewControllerDelegate {
)
coordinator.didCancel = { [weak self] coordinator in
- guard let self else { return }
+ guard let self = self else { return }
navigationController.popViewController(animated: true)
coordinator.removeFromParent()
}
diff --git a/ios/MullvadVPN/Extensions/Coordinator+Router.swift b/ios/MullvadVPN/Extensions/Coordinator+Router.swift
new file mode 100644
index 0000000000..dba2f5fed5
--- /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<AppRoute>? {
+ var appCoordinator = self
+
+ while let parentCoordinator = appCoordinator.parent {
+ appCoordinator = parentCoordinator
+ }
+
+ return (appCoordinator as? ApplicationCoordinator)?.router
+ }
+}
diff --git a/ios/MullvadVPN/Operations/AlertPresenter.swift b/ios/MullvadVPN/Operations/AlertPresenter.swift
deleted file mode 100644
index 7c31017a4e..0000000000
--- 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
deleted file mode 100644
index 7dff3399f6..0000000000
--- a/ios/MullvadVPN/Operations/PresentAlertOperation.swift
+++ /dev/null
@@ -1,59 +0,0 @@
-//
-// PresentAlertOperation.swift
-// MullvadVPN
-//
-// Created by pronebird on 06/09/2021.
-// Copyright © 2021 Mullvad VPN AB. All rights reserved.
-//
-
-import Operations
-import UIKit
-
-final class PresentAlertOperation: AsyncOperation {
- private let alertController: CustomAlertViewController
- private let presentingController: UIViewController
- private let presentCompletion: (() -> Void)?
-
- init(
- alertController: CustomAlertViewController,
- presentingController: UIViewController,
- presentCompletion: (() -> Void)? = nil
- ) {
- self.alertController = alertController
- self.presentingController = presentingController
- self.presentCompletion = presentCompletion
-
- super.init(dispatchQueue: .main)
- }
-
- override func operationDidCancel() {
- // Guard against trying to dismiss the alert when operation hasn't started yet.
- guard isExecuting else { return }
-
- // Guard against dismissing controller during transition.
- if !alertController.isBeingPresented, !alertController.isBeingDismissed {
- dismissAndFinish()
- }
- }
-
- override func main() {
- alertController.didDismiss = { [weak self] in
- self?.finish()
- }
-
- presentingController.present(alertController, animated: true) {
- self.presentCompletion?()
-
- // Alert operation was cancelled during transition?
- if self.isCancelled {
- self.dismissAndFinish()
- }
- }
- }
-
- private func dismissAndFinish() {
- alertController.dismiss(animated: false) {
- self.finish()
- }
- }
-}
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index b8641b6845..bb1adca7a5 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -187,28 +187,33 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand
// MARK: - SettingsMigrationUIHandler
func showMigrationError(_ error: Error, completionHandler: @escaping () -> Void) {
- let alertController = CustomAlertViewController(
+ guard let appCoordinator else {
+ completionHandler()
+ return
+ }
+
+ let presentation = AlertPresentation(
+ id: "settings-migration-error-alert",
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()
- }
+ let presenter = AlertPresenter(context: appCoordinator)
+ presenter.showAlert(presentation: presentation, animated: true)
}
private static func migrationErrorReason(_ error: Error) -> String {
diff --git a/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift b/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift
index be9bbce2b1..ae7ac01991 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift
@@ -75,6 +75,10 @@ class AccountDeviceRow: UIView {
fatalError("init(coder:) has not been implemented")
}
+ func setButtons(enabled: Bool) {
+ infoButton.isEnabled = enabled
+ }
+
@objc private func didTapInfoButton() {
infoButtonAction?()
}
diff --git a/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift b/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift
index 2f8ce5f037..07d5199c70 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift
@@ -108,6 +108,11 @@ class AccountNumberRow: UIView {
fatalError("init(coder:) has not been implemented")
}
+ func setButtons(enabled: Bool) {
+ showHideButton.isEnabled = enabled
+ copyButton.isEnabled = enabled
+ }
+
// MARK: - Private
private func updateView() {
diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
index 4f74878ced..435123dc4e 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
@@ -190,6 +190,8 @@ class AccountViewController: UIViewController {
contentView.purchaseButton.isLoading = productState.isFetching
purchaseButton.isEnabled = productState.isReceived && isInteractionEnabled
+ contentView.accountDeviceRow.setButtons(enabled: isInteractionEnabled)
+ contentView.accountTokenRowView.setButtons(enabled: isInteractionEnabled)
contentView.restorePurchasesButton.isEnabled = isInteractionEnabled
contentView.logoutButton.isEnabled = isInteractionEnabled
contentView.redeemVoucherButton.isEnabled = isInteractionEnabled
diff --git a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift
index 8d120a6a5b..0192f3fdd3 100644
--- a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift
+++ b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift
@@ -7,36 +7,33 @@
//
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 alertContext: any Presenting
func showAlertForError(
_ error: StorePaymentManagerError,
context: REST.CreateApplePaymentResponse.Context,
completion: (() -> Void)? = nil
) {
- let alertController = CustomAlertViewController(
+ let presentation = AlertPresentation(
+ id: "payment-error-alert",
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)
+ let presenter = AlertPresenter(context: alertContext)
+ presenter.showAlert(presentation: presentation, animated: true)
}
func showAlertForResponse(
@@ -49,20 +46,23 @@ class PaymentAlertPresenter {
return
}
- let alertController = CustomAlertViewController(
+ let presentation = AlertPresentation(
+ id: "payment-response-alert",
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)
+ let presenter = AlertPresenter(context: alertContext)
+ presenter.showAlert(presentation: 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 0000000000..9fcaac8429
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift
@@ -0,0 +1,45 @@
+//
+// AlertPresentation.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2023-08-23.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Routing
+
+struct AlertMetadata {
+ let presentation: AlertPresentation
+ let context: Presenting
+}
+
+struct AlertAction {
+ let title: String
+ let style: AlertActionStyle
+ var handler: (() -> Void)?
+}
+
+struct AlertPresentation: Identifiable, CustomDebugStringConvertible {
+ let id: String
+
+ var header: String?
+ var icon: AlertIcon?
+ var title: String?
+ let message: String?
+ let buttons: [AlertAction]
+
+ var debugDescription: String {
+ return id
+ }
+}
+
+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 0000000000..db2c0ff971
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift
@@ -0,0 +1,31 @@
+//
+// AlertPresenter.swift
+// MullvadVPN
+//
+// Created by pronebird on 04/06/2020.
+// Copyright © 2020 Mullvad VPN AB. All rights reserved.
+//
+
+import Routing
+
+struct AlertPresenter {
+ let context: any Presenting
+
+ func showAlert(presentation: AlertPresentation, animated: Bool) {
+ context.applicationRouter?.presentAlert(
+ route: .alert(presentation.id),
+ animated: animated,
+ metadata: AlertMetadata(presentation: presentation, context: context)
+ )
+ }
+
+ func dismissAlert(presentation: AlertPresentation, animated: Bool) {
+ context.applicationRouter?.dismiss(.alert(presentation.id), animated: animated)
+ }
+}
+
+extension ApplicationRouter {
+ func presentAlert(route: RouteType, animated: Bool, metadata: AlertMetadata) {
+ present(route, animated: animated, metadata: metadata)
+ }
+}
diff --git a/ios/MullvadVPN/Classes/CustomAlertViewController.swift b/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift
index cb6b92d1df..0a3fced671 100644
--- a/ios/MullvadVPN/Classes/CustomAlertViewController.swift
+++ b/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift
@@ -1,5 +1,5 @@
//
-// CustomAlertController.swift
+// AlertViewController.swift
// MullvadVPN
//
// Created by Jon Petersson on 2023-05-19.
@@ -8,41 +8,40 @@
import UIKit
-class CustomAlertViewController: UIViewController {
- typealias Handler = () -> Void
-
- enum Icon {
- case alert
- case info
- case spinner
+enum AlertActionStyle {
+ case `default`
+ case destructive
- 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
- }
+ fileprivate var buttonStyle: AppButton.Style {
+ switch self {
+ case .default:
+ return .default
+ case .destructive:
+ return .danger
}
}
+}
- enum ActionStyle {
- case `default`
- case destructive
+enum AlertIcon {
+ case alert
+ case info
+ case spinner
- fileprivate var buttonStyle: AppButton.Style {
- switch self {
- case .default:
- return .default
- case .destructive:
- return .danger
- }
+ 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 fa8e1f0945..7675490333 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)
}
@@ -89,7 +89,7 @@ class DeviceManagementViewController: UIViewController, RootContainment {
completionHandler: ((Result<Void, Error>) -> Void)? = nil
) {
interactor.getDevices { [weak self] result in
- guard let self else { return }
+ guard let self = self else { return }
if let devices = result.value {
setDevices(devices, animated: animateUpdates)
@@ -130,7 +130,9 @@ class DeviceManagementViewController: UIViewController, RootContainment {
return
}
- deleteDevice(identifier: device.id) { error in
+ deleteDevice(identifier: device.id) { [weak self] error in
+ guard let self = self else { return }
+
if let error {
self.showErrorAlert(
title: NSLocalizedString(
@@ -157,74 +159,75 @@ class DeviceManagementViewController: UIViewController, RootContainment {
}
private func showErrorAlert(title: String, error: Error) {
- let alertController = CustomAlertViewController(
+ let presentation = AlertPresentation(
+ id: "delete-device-error-alert",
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(
+ id: "logout-confirmation-alert",
+ 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) {
interactor.deleteDevice(identifier) { [weak self] completion in
- guard let self else { return }
+ guard let self = self else { return }
switch completion {
case .success:
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
index ef05999fe4..2a267142ae 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,24 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
}
private func showContentBlockerInfo(with message: String) {
- let alertController = CustomAlertViewController(
+ let presentation = AlertPresentation(
+ id: "preferences-content-blockers-alert",
+ 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 {
@@ -150,8 +154,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
case .wireGuardPorts:
let portsString = humanReadablePortRepresentation(
- interactor.cachedRelays?.relays.wireguard
- .portRanges ?? []
+ interactor.cachedRelays?.relays.wireguard.portRanges ?? []
)
message = String(
diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift
index 19751e8ed7..1bbd63f101 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?
@@ -44,8 +45,9 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate {
"SUBHEAD_LABEL",
tableName: "ProblemReport",
value: """
- To help you more effectively, your app’s log file will be attached to this message. \
- Your data will remain secure and private, as it is anonymised before being sent over an encrypted channel.
+ To help you more effectively, your app’s log file will be attached to \
+ this message. Your data will remain secure and private, as it is anonymised \
+ before being sent over an encrypted channel.
""",
comment: ""
)
@@ -86,8 +88,8 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate {
"DESCRIPTION_TEXTVIEW_PLACEHOLDER",
tableName: "ProblemReport",
value: """
- To assist you better, please write in English or Swedish and include \
- which country you are connecting from.
+ To assist you better, please write in English or Swedish and \
+ include which country you are connecting from.
""",
comment: ""
)
@@ -193,8 +195,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)
}
@@ -463,48 +466,47 @@ 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(
+ id: "problem-report-alert",
+ 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/Operations/AsyncOperation.swift b/ios/Operations/AsyncOperation.swift
index 638d6ff415..034f17f199 100644
--- a/ios/Operations/AsyncOperation.swift
+++ b/ios/Operations/AsyncOperation.swift
@@ -71,7 +71,6 @@ open class AsyncOperation: Operation {
get {
stateLock.lock()
defer { stateLock.unlock() }
-
return _state
}
set(newState) {
@@ -88,7 +87,6 @@ open class AsyncOperation: Operation {
get {
stateLock.lock()
defer { stateLock.unlock() }
-
return __isCancelled
}
set {
@@ -182,7 +180,6 @@ open class AsyncOperation: Operation {
public final var conditions: [OperationCondition] {
operationLock.lock()
defer { operationLock.unlock() }
-
return _conditions
}
diff --git a/ios/Routing/Coordinator.swift b/ios/Routing/Coordinator.swift
index 217a951aef..46e343e648 100644
--- a/ios/Routing/Coordinator.swift
+++ b/ios/Routing/Coordinator.swift
@@ -84,13 +84,23 @@ 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 }
}
/**
+ Protocol describing `Presentable` coordinators that can be popped from a navigation stack.
+ */
+public protocol Poppable: Presentable {
+ func popFromNavigationStack(
+ animated: Bool,
+ completion: (() -> Void)?
+ )
+}
+
+/**
Protocol describing coordinators that provide modal presentation context.
*/
public protocol Presenting: Coordinator {
@@ -100,6 +110,15 @@ public protocol Presenting: Coordinator {
var presentationContext: UIViewController { get }
}
+extension Presenting where Self: Presentable {
+ /**
+ View controller providing modal presentation context.
+ */
+ public var presentationContext: UIViewController {
+ return presentedViewController
+ }
+}
+
extension Presenting {
/**
Present child coordinator.
@@ -134,12 +153,22 @@ extension Presenting {
addChild(child)
- presentationContext.present(
+ topmostPresentationContext(from: presentationContext).present(
child.presentedViewController,
animated: animated,
completion: completion
)
}
+
+ private func topmostPresentationContext(from: UIViewController) -> UIViewController {
+ var context = presentationContext
+
+ while let childContext = context.presentedViewController, context != childContext {
+ context = childContext
+ }
+
+ return context
+ }
}
extension Presentable {
diff --git a/ios/Routing/Router/AppRouteProtocol.swift b/ios/Routing/Router/AppRouteProtocol.swift
index 1843183958..85fbd53028 100644
--- a/ios/Routing/Router/AppRouteProtocol.swift
+++ b/ios/Routing/Router/AppRouteProtocol.swift
@@ -35,6 +35,10 @@ extension AppRouteGroupProtocol {
public static func < (lhs: Self, rhs: Self) -> Bool {
lhs.modalLevel < rhs.modalLevel
}
+
+ public static func <= (lhs: Self, rhs: Self) -> Bool {
+ lhs.modalLevel <= rhs.modalLevel
+ }
}
/**
diff --git a/ios/Routing/Router/ApplicationRouter.swift b/ios/Routing/Router/ApplicationRouter.swift
index 2de0cbc3ff..d565875143 100644
--- a/ios/Routing/Router/ApplicationRouter.swift
+++ b/ios/Routing/Router/ApplicationRouter.swift
@@ -53,10 +53,11 @@ public final class ApplicationRouter<RouteType: AppRouteProtocol> {
/**
Enqueue route for presetnation.
*/
- public func present(_ route: RouteType, animated: Bool = true) {
+ public func present(_ route: RouteType, animated: Bool = true, metadata: Any? = nil) {
enqueue(PendingRoute(
operation: .present(route),
- animated: animated
+ animated: animated,
+ metadata: metadata
))
}
@@ -81,7 +82,7 @@ public final class ApplicationRouter<RouteType: AppRouteProtocol> {
}
private func enqueue(_ pendingRoute: PendingRoute<RouteType>) {
- logger.debug("Enqueue \(pendingRoute.operation).")
+ logger.debug("\(pendingRoute.operation).")
pendingRoutes.append(pendingRoute)
@@ -93,6 +94,7 @@ public final class ApplicationRouter<RouteType: AppRouteProtocol> {
private func presentRoute(
_ route: RouteType,
animated: Bool,
+ metadata: Any?,
completion: @escaping (PendingPresentationResult) -> Void
) {
/**
@@ -117,7 +119,7 @@ public final class ApplicationRouter<RouteType: AppRouteProtocol> {
}
/**
- Drop duplicate routes.
+ Drop duplicate exclusive routes.
*/
if route.isExclusive, modalStack.contains(route.routeGroup) {
completion(.drop)
@@ -135,8 +137,15 @@ public final class ApplicationRouter<RouteType: AppRouteProtocol> {
/**
Check if route can be presented above the last route in the modal stack.
*/
- if let lastRouteGroup = modalStack.last, route.routeGroup.isModal,
- (lastRouteGroup > route.routeGroup) || (route.isExclusive && lastRouteGroup == route.routeGroup) {
+ if let
+ // Get current modal route.
+ lastRouteGroup = modalStack.last,
+ // Check if incoming route is modal.
+ route.routeGroup.isModal,
+ // Check whether incoming route can be presented on top of current.
+ (lastRouteGroup > route.routeGroup) ||
+ // OR, check whether incoming exclusive route can be presented on top of current.
+ (lastRouteGroup >= route.routeGroup && route.isExclusive) {
completion(.blockedByModalContext)
return
}
@@ -145,7 +154,9 @@ public final class ApplicationRouter<RouteType: AppRouteProtocol> {
Consult with delegate whether the route should still be presented.
*/
if delegate.applicationRouter(self, shouldPresent: route) {
- delegate.applicationRouter(self, route: route, animated: animated) { coordinator in
+ let context = RoutePresentationContext(route: route, isAnimated: animated, metadata: metadata)
+
+ delegate.applicationRouter(self, presentWithContext: context, animated: animated) { coordinator in
/*
Synchronize router when modal controllers are removed by swipe.
*/
@@ -276,7 +287,7 @@ public final class ApplicationRouter<RouteType: AppRouteProtocol> {
switch pendingRoute.operation {
case let .present(route):
- presentRoute(route, animated: pendingRoute.animated) { result in
+ presentRoute(route, animated: pendingRoute.animated, metadata: pendingRoute.metadata) { result in
switch result {
case .success, .drop:
self.finishPendingRoute(pendingRoute)
diff --git a/ios/Routing/Router/ApplicationRouterDelegate.swift b/ios/Routing/Router/ApplicationRouterDelegate.swift
index ecccb15415..a98870d303 100644
--- a/ios/Routing/Router/ApplicationRouterDelegate.swift
+++ b/ios/Routing/Router/ApplicationRouterDelegate.swift
@@ -19,7 +19,7 @@ public protocol ApplicationRouterDelegate<RouteType>: AnyObject {
*/
func applicationRouter(
_ router: ApplicationRouter<RouteType>,
- route: RouteType,
+ presentWithContext context: RoutePresentationContext<RouteType>,
animated: Bool,
completion: @escaping (Coordinator) -> Void
)
diff --git a/ios/Routing/Router/ApplicationRouterTypes.swift b/ios/Routing/Router/ApplicationRouterTypes.swift
index 1dfd128c0a..7be142adaf 100644
--- a/ios/Routing/Router/ApplicationRouterTypes.swift
+++ b/ios/Routing/Router/ApplicationRouterTypes.swift
@@ -11,9 +11,16 @@ import Foundation
/**
Struct describing a routing request for presentation or dismissal.
*/
-struct PendingRoute<RouteType: AppRouteProtocol>: Equatable {
+struct PendingRoute<RouteType: AppRouteProtocol> {
var operation: RouteOperation<RouteType>
var animated: Bool
+ var metadata: Any?
+}
+
+extension PendingRoute: Equatable {
+ static func == (lhs: PendingRoute<RouteType>, rhs: PendingRoute<RouteType>) -> Bool {
+ lhs.operation == rhs.operation
+ }
}
/**
@@ -69,7 +76,7 @@ enum PendingDismissalResult {
/**
Enum describing operation over the route.
*/
-enum RouteOperation<RouteType: AppRouteProtocol>: Equatable {
+enum RouteOperation<RouteType: AppRouteProtocol>: Equatable, CustomDebugStringConvertible {
/**
Present route.
*/
@@ -91,12 +98,23 @@ enum RouteOperation<RouteType: AppRouteProtocol>: 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<RouteType: AppRouteProtocol>: Equatable {
+enum DismissMatch<RouteType: AppRouteProtocol>: Equatable, CustomDebugStringConvertible {
case group(RouteType.RouteGroupType)
case singleRoute(RouteType)
@@ -111,6 +129,15 @@ enum DismissMatch<RouteType: AppRouteProtocol>: Equatable {
return route.routeGroup
}
}
+
+ var debugDescription: String {
+ switch self {
+ case let .group(group):
+ return "\(group)"
+ case let .singleRoute(route):
+ return "\(route)"
+ }
+ }
}
/**
@@ -142,6 +169,26 @@ public struct RouteDismissalContext<RouteType: AppRouteProtocol> {
}
/**
+ Struct holding information used by delegate to perform presentation of a specific route.
+ */
+public struct RoutePresentationContext<RouteType: AppRouteProtocol> {
+ /**
+ Route that's being presented.
+ */
+ public var route: RouteType
+
+ /**
+ Whether transition is animated.
+ */
+ public var isAnimated: Bool
+
+ /**
+ Metadata associated with the route.
+ */
+ public var metadata: Any?
+}
+
+/**
Struct holding information used by delegate to perform sub-navigation of the route in subject.
*/
public struct RouteSubnavigationContext<RouteType: AppRouteProtocol> {
diff --git a/ios/RoutingTests/RouterBlockDelegate.swift b/ios/RoutingTests/RouterBlockDelegate.swift
index b1c1f58908..977454b7e4 100644
--- a/ios/RoutingTests/RouterBlockDelegate.swift
+++ b/ios/RoutingTests/RouterBlockDelegate.swift
@@ -10,7 +10,7 @@ import Foundation
import Routing
class RouterBlockDelegate<RouteType: AppRouteProtocol>: ApplicationRouterDelegate {
- var handleRoute: ((RouteType, Bool, (Coordinator) -> Void) -> Void)?
+ var handleRoute: ((RoutePresentationContext<RouteType>, Bool, (Coordinator) -> Void) -> Void)?
var handleDismiss: ((RouteDismissalContext<RouteType>, () -> Void) -> Void)?
var shouldPresent: ((RouteType) -> Bool)?
var shouldDismiss: ((RouteDismissalContext<RouteType>) -> Bool)?
@@ -18,11 +18,11 @@ class RouterBlockDelegate<RouteType: AppRouteProtocol>: ApplicationRouterDelegat
func applicationRouter(
_ router: ApplicationRouter<RouteType>,
- route: RouteType,
+ presentWithContext context: RoutePresentationContext<RouteType>,
animated: Bool,
completion: @escaping (Coordinator) -> Void
) {
- handleRoute?(route, animated, completion) ?? completion(Coordinator())
+ handleRoute?(context, animated, completion) ?? completion(Coordinator())
}
func applicationRouter(