summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-08-23 13:50:57 +0200
committerAndrej Mihajlov <and@mullvad.net>2023-08-23 13:50:57 +0200
commit258a7667e5dab869e7d714fb862ec8f18453907a (patch)
treec290ae27695a0ecc920862bdccddad79d7ec6285
parent3413af0cf834f60259fe11d4b45094defd68aa88 (diff)
parenta02978a7ac0c1e064c5ceb06d778afb739f48ac2 (diff)
downloadmullvadvpn-258a7667e5dab869e7d714fb862ec8f18453907a.tar.xz
mullvadvpn-258a7667e5dab869e7d714fb862ec8f18453907a.zip
Merge branch 'adding-in-app-purchase-in-new-account-flow-ios-228'
-rw-r--r--ios/CHANGELOG.md1
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj64
-rw-r--r--ios/MullvadVPN/Classes/AppPreferences.swift66
-rw-r--r--ios/MullvadVPN/Classes/ChangeLog.swift62
-rw-r--r--ios/MullvadVPN/Classes/TermsOfService.swift25
-rw-r--r--ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift17
-rw-r--r--ios/MullvadVPN/Coordinators/App/AccountRedeemingVoucherCoordinator.swift34
-rw-r--r--ios/MullvadVPN/Coordinators/App/AddCreditSucceededCoordinator.swift76
-rw-r--r--ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift82
-rw-r--r--ios/MullvadVPN/Coordinators/App/ChangeLogCoordinator.swift54
-rw-r--r--ios/MullvadVPN/Coordinators/App/InAppPurchaseCoordinator.swift71
-rw-r--r--ios/MullvadVPN/Coordinators/App/LoginCoordinator.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/App/OutOfTimeCoordinator.swift6
-rw-r--r--ios/MullvadVPN/Coordinators/App/SettingsRedeemVoucherCoordinator.swift23
-rw-r--r--ios/MullvadVPN/Coordinators/App/SetupAccountCompletedCoordinator.swift10
-rw-r--r--ios/MullvadVPN/Coordinators/App/TermsOfServiceCoordinator.swift3
-rw-r--r--ios/MullvadVPN/Coordinators/App/WelcomeCoordinator.swift63
-rw-r--r--ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift69
-rw-r--r--ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift17
-rw-r--r--ios/MullvadVPN/Routing/AppRoutes.swift4
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift3
-rw-r--r--ios/MullvadVPN/SettingsManager/StoredAccountData.swift8
-rw-r--r--ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift3
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift3
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift2
-rw-r--r--ios/MullvadVPN/View controllers/ChangeLog/ChangeLogInteractor.swift54
-rw-r--r--ios/MullvadVPN/View controllers/ChangeLog/ChangeLogViewModel.swift48
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedController.swift (renamed from ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedViewController.swift)30
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift46
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift32
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift82
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift40
-rw-r--r--ios/MullvadVPN/View controllers/Login/LoginInteractor.swift5
-rw-r--r--ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift8
-rw-r--r--ios/MullvadVPN/View controllers/RedeemVoucher/AddCreditSucceededViewController.swift (renamed from ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherSucceededViewController.swift)45
-rw-r--r--ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift18
36 files changed, 839 insertions, 337 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md
index 3511978865..5b2571b5fc 100644
--- a/ios/CHANGELOG.md
+++ b/ios/CHANGELOG.md
@@ -25,6 +25,7 @@ Line wrap the file at 100 chars. Th
### Added
- Allow redeeming vouchers in account view.
- Allow deleting account in account view.
+- Add new account flow
## [2023.3 - 2023-07-15]
### Added
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 415298d98f..0e9d791882 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -116,7 +116,6 @@
584D26C6270C8741004EA533 /* SettingsDNSTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */; };
584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */; };
584F99202902CBDD001F858D /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; };
- 5859A55329CD9B1300F66591 /* ChangeLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5859A55229CD9B1300F66591 /* ChangeLog.swift */; };
5859A55529CD9DD900F66591 /* changes.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5859A55429CD9DD800F66591 /* changes.txt */; };
585A02E92A4B283000C6CAFF /* TCPUnsafeListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */; };
585A02EB2A4B285800C6CAFF /* UDPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */; };
@@ -153,7 +152,6 @@
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; };
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */; };
58727283265D173C00F315B2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 58727282265D173C00F315B2 /* LaunchScreen.storyboard */; };
- 5872D6E8286304DE00DB5F4E /* TermsOfService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */; };
587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587425C02299833500CA2045 /* RootContainerViewController.swift */; };
5875960A26F371FC00BF6711 /* Tunnel+Messaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875960926F371FC00BF6711 /* Tunnel+Messaging.swift */; };
5877D70F282137E8002FCFC7 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; };
@@ -461,10 +459,12 @@
F028A5492A336E8500C0CAA3 /* VoucherTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A5482A336E8500C0CAA3 /* VoucherTextField.swift */; };
F028A54B2A3370FA00C0CAA3 /* RedeemVoucherContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A54A2A3370FA00C0CAA3 /* RedeemVoucherContentView.swift */; };
F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */; };
- F028A56C2A34D8E600C0CAA3 /* RedeemVoucherSucceededViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56B2A34D8E600C0CAA3 /* RedeemVoucherSucceededViewController.swift */; };
+ F028A56C2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */; };
F028A56E2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56D2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift */; };
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; };
F041CD562A38B0B7001B703B /* SettingsRedeemVoucherCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F041CD552A38B0B7001B703B /* SettingsRedeemVoucherCoordinator.swift */; };
+ F0465B5C2A7927B40004089E /* AddCreditSucceededCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0465B5B2A7927B40004089E /* AddCreditSucceededCoordinator.swift */; };
+ F04FBE612A8379EE009278D7 /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04FBE602A8379EE009278D7 /* AppPreferences.swift */; };
F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */; };
F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */; };
F07C0A052A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07C0A042A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift */; };
@@ -472,17 +472,21 @@
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; };
F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; };
F0C6FA812A66E23300F521F0 /* DeleteAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */; };
+ F0C6FA832A6A729500F521F0 /* InAppPurchaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA822A6A729500F521F0 /* InAppPurchaseCoordinator.swift */; };
+ F0C6FA852A6A733700F521F0 /* InAppPurchaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */; };
F0E3618B2A4ADD2F00AEEF2B /* WelcomeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */; };
F0E8CC032A4C753B007ED3B4 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */; };
F0E8CC052A4CC88F007ED3B4 /* WelcomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC042A4CC88F007ED3B4 /* WelcomeCoordinator.swift */; };
F0E8CC0A2A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */; };
- F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift */; };
+ F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedController.swift */; };
F0E8E4BB2A56C9F100ED26A3 /* WelcomeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4BA2A56C9F100ED26A3 /* WelcomeInteractor.swift */; };
F0E8E4C12A602CCB00ED26A3 /* AccountDeletionContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C02A602CCB00ED26A3 /* AccountDeletionContentView.swift */; };
F0E8E4C32A602E0D00ED26A3 /* AccountDeletionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C22A602E0D00ED26A3 /* AccountDeletionViewModel.swift */; };
F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C42A60499100ED26A3 /* AccountDeletionViewController.swift */; };
F0E8E4C72A604CBE00ED26A3 /* AccountDeletionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C62A604CBE00ED26A3 /* AccountDeletionCoordinator.swift */; };
F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C82A604E7400ED26A3 /* AccountDeletionInteractor.swift */; };
+ F0EF50D32A8FA47E0031E8DF /* ChangeLogInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EF50D22A8FA47E0031E8DF /* ChangeLogInteractor.swift */; };
+ F0EF50D52A949F8E0031E8DF /* ChangeLogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -1023,7 +1027,6 @@
584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDNSTextCell.swift; sourceTree = "<group>"; };
584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Markdown.swift"; sourceTree = "<group>"; };
58561C98239A5D1500BD6B5E /* IPv4Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4Endpoint.swift; sourceTree = "<group>"; };
- 5859A55229CD9B1300F66591 /* ChangeLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLog.swift; sourceTree = "<group>"; };
5859A55429CD9DD800F66591 /* changes.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = changes.txt; sourceTree = "<group>"; };
585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPUnsafeListener.swift; sourceTree = "<group>"; };
585A02EA2A4B285800C6CAFF /* UDPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnection.swift; sourceTree = "<group>"; };
@@ -1059,7 +1062,6 @@
5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLog.swift; sourceTree = "<group>"; };
5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+IPAddress.swift"; sourceTree = "<group>"; };
58727282265D173C00F315B2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
- 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfService.swift; sourceTree = "<group>"; };
587425C02299833500CA2045 /* RootContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootContainerViewController.swift; sourceTree = "<group>"; };
5875960926F371FC00BF6711 /* Tunnel+Messaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tunnel+Messaging.swift"; sourceTree = "<group>"; };
5877F94D2A0A59AA0052D9E9 /* NotificationResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationResponse.swift; sourceTree = "<group>"; };
@@ -1292,10 +1294,12 @@
F028A5482A336E8500C0CAA3 /* VoucherTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoucherTextField.swift; sourceTree = "<group>"; };
F028A54A2A3370FA00C0CAA3 /* RedeemVoucherContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherContentView.swift; sourceTree = "<group>"; };
F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewController.swift; sourceTree = "<group>"; };
- F028A56B2A34D8E600C0CAA3 /* RedeemVoucherSucceededViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherSucceededViewController.swift; sourceTree = "<group>"; };
+ F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddCreditSucceededViewController.swift; sourceTree = "<group>"; };
F028A56D2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherInteractor.swift; sourceTree = "<group>"; };
F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = "<group>"; };
F041CD552A38B0B7001B703B /* SettingsRedeemVoucherCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRedeemVoucherCoordinator.swift; sourceTree = "<group>"; };
+ F0465B5B2A7927B40004089E /* AddCreditSucceededCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCreditSucceededCoordinator.swift; sourceTree = "<group>"; };
+ F04FBE602A8379EE009278D7 /* AppPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = "<group>"; };
F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = "<group>"; };
F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherOperation.swift; sourceTree = "<group>"; };
F07C0A042A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRedeemingVoucherCoordinator.swift; sourceTree = "<group>"; };
@@ -1303,17 +1307,21 @@
F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = "<group>"; };
F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderIdentifier.swift; sourceTree = "<group>"; };
F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountOperation.swift; sourceTree = "<group>"; };
+ F0C6FA822A6A729500F521F0 /* InAppPurchaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseCoordinator.swift; sourceTree = "<group>"; };
+ F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseInteractor.swift; sourceTree = "<group>"; };
F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeContentView.swift; sourceTree = "<group>"; };
F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
F0E8CC042A4CC88F007ED3B4 /* WelcomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeCoordinator.swift; sourceTree = "<group>"; };
F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedContentView.swift; sourceTree = "<group>"; };
- F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedViewController.swift; sourceTree = "<group>"; };
+ F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedController.swift; sourceTree = "<group>"; };
F0E8E4BA2A56C9F100ED26A3 /* WelcomeInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeInteractor.swift; sourceTree = "<group>"; };
F0E8E4C02A602CCB00ED26A3 /* AccountDeletionContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionContentView.swift; sourceTree = "<group>"; };
F0E8E4C22A602E0D00ED26A3 /* AccountDeletionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionViewModel.swift; sourceTree = "<group>"; };
F0E8E4C42A60499100ED26A3 /* AccountDeletionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionViewController.swift; sourceTree = "<group>"; };
F0E8E4C62A604CBE00ED26A3 /* AccountDeletionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionCoordinator.swift; sourceTree = "<group>"; };
F0E8E4C82A604E7400ED26A3 /* AccountDeletionInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionInteractor.swift; sourceTree = "<group>"; };
+ F0EF50D22A8FA47E0031E8DF /* ChangeLogInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeLogInteractor.swift; sourceTree = "<group>"; };
+ F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLogViewModel.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -1667,6 +1675,7 @@
583FE01629C196E8006E85F9 /* View controllers */ = {
isa = PBXGroup;
children = (
+ F0EF50D12A8FA47E0031E8DF /* ChangeLog */,
583FE02029C1A0B1006E85F9 /* Account */,
F0E8E4BF2A602C7D00ED26A3 /* AccountDeletion */,
F0E8E4B92A55593300ED26A3 /* CreationAccount */,
@@ -1896,8 +1905,8 @@
isa = PBXGroup;
children = (
587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */,
+ F04FBE602A8379EE009278D7 /* AppPreferences.swift */,
58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */,
- 5859A55229CD9B1300F66591 /* ChangeLog.swift */,
5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */,
7AE47E512A17972A000418DA /* CustomAlertViewController.swift */,
5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */,
@@ -1906,7 +1915,6 @@
582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */,
7AF9BE982A4E0FE900DBFEDB /* MarkdownStylingOptions.swift */,
58CC40EE24A601900019D96E /* ObserverList.swift */,
- 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */,
);
path = Classes;
sourceTree = "<group>";
@@ -2045,8 +2053,10 @@
7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */,
F0E8E4C62A604CBE00ED26A3 /* AccountDeletionCoordinator.swift */,
F07C0A042A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift */,
+ F0465B5B2A7927B40004089E /* AddCreditSucceededCoordinator.swift */,
58BBB39629717E0C00C8DB7C /* ApplicationCoordinator.swift */,
5878F50129CDB989003D4BE2 /* ChangeLogCoordinator.swift */,
+ F0C6FA822A6A729500F521F0 /* InAppPurchaseCoordinator.swift */,
58CAF9F92983E0C600BE19F7 /* LoginCoordinator.swift */,
583FE00D29C0D586006E85F9 /* OutOfTimeCoordinator.swift */,
5847D58C29B7740F008C3808 /* RevokedCoordinator.swift */,
@@ -2241,6 +2251,7 @@
58C7A4432A863F490060C66F /* PacketTunnelCoreTests */,
58CE5E61224146200008646E /* Products */,
584F991F2902CBDD001F858D /* Frameworks */,
+ F0EF50D02A8FA2C00031E8DF /* Recovered References */,
);
sourceTree = "<group>";
};
@@ -2477,7 +2488,7 @@
children = (
F028A54A2A3370FA00C0CAA3 /* RedeemVoucherContentView.swift */,
F028A56D2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift */,
- F028A56B2A34D8E600C0CAA3 /* RedeemVoucherSucceededViewController.swift */,
+ F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */,
F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */,
F028A5482A336E8500C0CAA3 /* VoucherTextField.swift */,
);
@@ -2498,7 +2509,7 @@
isa = PBXGroup;
children = (
F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */,
- F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift */,
+ F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedController.swift */,
);
path = Completed;
sourceTree = "<group>";
@@ -2507,6 +2518,7 @@
isa = PBXGroup;
children = (
F0E8CC082A4EE0DC007ED3B4 /* Completed */,
+ F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */,
F0E361892A4ADCF500AEEF2B /* Welcome */,
);
path = CreationAccount;
@@ -2523,6 +2535,22 @@
path = AccountDeletion;
sourceTree = "<group>";
};
+ F0EF50D02A8FA2C00031E8DF /* Recovered References */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = "Recovered References";
+ sourceTree = "<group>";
+ };
+ F0EF50D12A8FA47E0031E8DF /* ChangeLog */ = {
+ isa = PBXGroup;
+ children = (
+ F0EF50D22A8FA47E0031E8DF /* ChangeLogInteractor.swift */,
+ F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */,
+ );
+ path = ChangeLog;
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -3470,9 +3498,11 @@
5864AF0729C78843005B0CD9 /* SettingsCellFactory.swift in Sources */,
587B75412668FD7800DEF7E9 /* AccountExpirySystemNotificationProvider.swift in Sources */,
587988C728A2A01F00E3DF54 /* AccountDataThrottling.swift in Sources */,
+ F04FBE612A8379EE009278D7 /* AppPreferences.swift in Sources */,
5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */,
7AF0419E29E957EB00D492DD /* AccountCoordinator.swift in Sources */,
5867771429097BCD006F721F /* PaymentState.swift in Sources */,
+ F0EF50D32A8FA47E0031E8DF /* ChangeLogInteractor.swift in Sources */,
F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */,
587D96742886D87C00CD8F1C /* DeviceManagementContentView.swift in Sources */,
58C3F4FB296C3AD500D72515 /* SettingsCoordinator.swift in Sources */,
@@ -3546,7 +3576,7 @@
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */,
582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */,
5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */,
- F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift in Sources */,
+ F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedController.swift in Sources */,
5846227726E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift in Sources */,
58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */,
58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */,
@@ -3570,6 +3600,8 @@
7A1A26432A2612AE00B978AA /* PaymentAlertPresenter.swift in Sources */,
58CCA01822426713004F3011 /* AccountViewController.swift in Sources */,
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */,
+ F0EF50D52A949F8E0031E8DF /* ChangeLogViewModel.swift in Sources */,
+ F0C6FA832A6A729500F521F0 /* InAppPurchaseCoordinator.swift in Sources */,
F0E8E4BB2A56C9F100ED26A3 /* WelcomeInteractor.swift in Sources */,
5878A27729093A4F0096FC88 /* StorePaymentBlockObserver.swift in Sources */,
5802EBC52A8E44AC00E5CE4C /* AppRoutes.swift in Sources */,
@@ -3614,6 +3646,7 @@
583FE01229C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift in Sources */,
58677712290976FB006F721F /* SettingsInteractor.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
+ F0C6FA852A6A733700F521F0 /* InAppPurchaseInteractor.swift in Sources */,
5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */,
583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */,
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */,
@@ -3631,7 +3664,6 @@
58B26E262943522400D5980C /* NotificationProvider.swift in Sources */,
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
5878A27329091D6D0096FC88 /* TunnelBlockObserver.swift in Sources */,
- 5872D6E8286304DE00DB5F4E /* TermsOfService.swift in Sources */,
58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */,
586891CD29D452E4002A8278 /* SafariCoordinator.swift in Sources */,
58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */,
@@ -3652,7 +3684,7 @@
58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */,
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */,
587D9676288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift in Sources */,
- F028A56C2A34D8E600C0CAA3 /* RedeemVoucherSucceededViewController.swift in Sources */,
+ F028A56C2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift in Sources */,
58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */,
F028A5492A336E8500C0CAA3 /* VoucherTextField.swift in Sources */,
58B9EB152489139B00095626 /* RESTError+Display.swift in Sources */,
@@ -3665,6 +3697,7 @@
5875960A26F371FC00BF6711 /* Tunnel+Messaging.swift in Sources */,
063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */,
587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */,
+ F0465B5C2A7927B40004089E /* AddCreditSucceededCoordinator.swift in Sources */,
5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */,
5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */,
587AD7C623421D7000E93A53 /* TunnelSettingsV1.swift in Sources */,
@@ -3683,7 +3716,6 @@
580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */,
58A8EE5A2976BFBB009C0F8D /* SKError+Localized.swift in Sources */,
7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */,
- 5859A55329CD9B1300F66591 /* ChangeLog.swift in Sources */,
58BBB39729717E0C00C8DB7C /* ApplicationCoordinator.swift in Sources */,
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */,
586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */,
diff --git a/ios/MullvadVPN/Classes/AppPreferences.swift b/ios/MullvadVPN/Classes/AppPreferences.swift
new file mode 100644
index 0000000000..4a790ba8fc
--- /dev/null
+++ b/ios/MullvadVPN/Classes/AppPreferences.swift
@@ -0,0 +1,66 @@
+//
+// AppPreferences.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-08-09.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol AppPreferencesDataSource {
+ var isShownOnboarding: Bool { set get }
+ var isAgreedToTermsOfService: Bool { get set }
+ var lastSeenChangeLogVersion: String { get set }
+}
+
+enum AppStorageKey: String {
+ case isShownOnboarding, isAgreedToTermsOfService, lastSeenChangeLogVersion
+}
+
+@propertyWrapper
+struct AppStorage<Value> {
+ let key: AppStorageKey
+ let defaultValue: Value
+ let container: UserDefaults
+
+ var wrappedValue: Value {
+ get {
+ let value = container.value(forKey: key.rawValue)
+ return value.flatMap { $0 as? Value } ?? defaultValue
+ }
+ set {
+ if let anyOptional = newValue as? AnyOptional,
+ anyOptional.isNil {
+ container.removeObject(forKey: key.rawValue)
+ } else {
+ container.set(newValue, forKey: key.rawValue)
+ }
+ }
+ }
+
+ init(wrappedValue: Value, _ key: AppStorageKey, container: UserDefaults = .standard) {
+ self.defaultValue = wrappedValue
+ self.container = container
+ self.key = key
+ }
+}
+
+final class AppPreferences: AppPreferencesDataSource {
+ @AppStorage(.isShownOnboarding)
+ var isShownOnboarding = true
+
+ @AppStorage(.isAgreedToTermsOfService)
+ var isAgreedToTermsOfService = false
+
+ @AppStorage(.lastSeenChangeLogVersion)
+ var lastSeenChangeLogVersion = ""
+}
+
+protocol AnyOptional {
+ var isNil: Bool { get }
+}
+
+extension Optional: AnyOptional {
+ var isNil: Bool { self == nil }
+}
diff --git a/ios/MullvadVPN/Classes/ChangeLog.swift b/ios/MullvadVPN/Classes/ChangeLog.swift
deleted file mode 100644
index d98fe903da..0000000000
--- a/ios/MullvadVPN/Classes/ChangeLog.swift
+++ /dev/null
@@ -1,62 +0,0 @@
-//
-// ChangeLog.swift
-// MullvadVPN
-//
-// Created by pronebird on 24/03/2023.
-// Copyright © 2023 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-enum ChangeLog {
- private static let userDefaultsKey = "lastSeenChangeLogVersion"
-
- /**
- Returns `true` if changelog for current application version was already seen by user, otherwise
- `false`.
- */
- static var isSeen: Bool {
- let version = UserDefaults.standard.string(forKey: Self.userDefaultsKey)
-
- return version == Bundle.main.shortVersion
- }
-
- /**
- Marks changelog for current application version as seen in user defaults.
- */
- static func markAsSeen() {
- UserDefaults.standard.set(Bundle.main.shortVersion, forKey: Self.userDefaultsKey)
- }
-
- /**
- Marks changelog as unseen. Removes an entry from user defaults.
- */
- static func markAsUnseen() {
- UserDefaults.standard.removeObject(forKey: Self.userDefaultsKey)
- }
-
- /**
- Reads changelog file from bundle and returns its contents as a string.
- */
- static func readFromFile() throws -> String {
- try String(contentsOfFile: try getPathToChangesFile())
- .split(whereSeparator: { $0.isNewline })
- .compactMap { line in
- let trimmedString = line.trimmingCharacters(in: .whitespaces)
-
- return trimmedString.isEmpty ? nil : trimmedString
- }
- .joined(separator: "\n")
- }
-
- /**
- Returns path to changelog file in bundle.
- */
- static func getPathToChangesFile() throws -> String {
- if let filePath = Bundle.main.path(forResource: "changes", ofType: "txt") {
- return filePath
- } else {
- throw CocoaError(.fileNoSuchFile)
- }
- }
-}
diff --git a/ios/MullvadVPN/Classes/TermsOfService.swift b/ios/MullvadVPN/Classes/TermsOfService.swift
deleted file mode 100644
index 7c00ca4fc0..0000000000
--- a/ios/MullvadVPN/Classes/TermsOfService.swift
+++ /dev/null
@@ -1,25 +0,0 @@
-//
-// TermsOfService.swift
-// MullvadVPN
-//
-// Created by pronebird on 22/06/2022.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-enum TermsOfService {
- private static let userDefaultsKey = "isAgreedToTermsOfService"
-
- static var isAgreed: Bool {
- UserDefaults.standard.bool(forKey: userDefaultsKey)
- }
-
- static func setAgreed() {
- UserDefaults.standard.set(true, forKey: userDefaultsKey)
- }
-
- static func unsetAgreed() {
- UserDefaults.standard.set(false, forKey: userDefaultsKey)
- }
-}
diff --git a/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift
index 934e0a7199..08e5a0e0b6 100644
--- a/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift
@@ -34,7 +34,6 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
}
var didFinish: ((AccountCoordinator, AccountDismissReason) -> Void)?
- var didAddMoreCredit: ((AccountCoordinator, AddedMoreCreditOption) -> Void)?
init(
navigationController: UINavigationController,
@@ -81,10 +80,8 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
navigationController: CustomNavigationController(),
interactor: RedeemVoucherInteractor(tunnelManager: interactor.tunnelManager)
)
- coordinator.didFinish = { [weak self] redeemVoucherCoordinator in
+ coordinator.didFinish = { redeemVoucherCoordinator in
redeemVoucherCoordinator.dismiss(animated: true)
- guard let self else { return }
- self.didAddMoreCredit?(self, .redeemingVoucher)
}
coordinator.didCancel = { redeemVoucherCoordinator in
redeemVoucherCoordinator.dismiss(animated: true)
@@ -96,7 +93,11 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
animated: true,
configuration: ModalPresentationConfiguration(
preferredContentSize: UIMetrics.SettingsRedeemVoucher.preferredContentSize,
- modalPresentationStyle: .formSheet
+ modalPresentationStyle: .custom,
+ transitioningDelegate: FormSheetTransitioningDelegate(options: FormSheetPresentationOptions(
+ useFullScreenPresentationInCompactWidth: false,
+ adjustViewWhenKeyboardAppears: true
+ ))
)
)
}
@@ -123,7 +124,11 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
animated: true,
configuration: ModalPresentationConfiguration(
preferredContentSize: UIMetrics.AccountDeletion.preferredContentSize,
- modalPresentationStyle: .formSheet
+ modalPresentationStyle: .custom,
+ transitioningDelegate: FormSheetTransitioningDelegate(options: FormSheetPresentationOptions(
+ useFullScreenPresentationInCompactWidth: true,
+ adjustViewWhenKeyboardAppears: false
+ ))
)
)
}
diff --git a/ios/MullvadVPN/Coordinators/App/AccountRedeemingVoucherCoordinator.swift b/ios/MullvadVPN/Coordinators/App/AccountRedeemingVoucherCoordinator.swift
index 160e4ff329..f81c29c421 100644
--- a/ios/MullvadVPN/Coordinators/App/AccountRedeemingVoucherCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/AccountRedeemingVoucherCoordinator.swift
@@ -36,33 +36,23 @@ class AccountRedeemingVoucherCoordinator: Coordinator, Presentable {
extension AccountRedeemingVoucherCoordinator: RedeemVoucherViewControllerDelegate {
func redeemVoucherDidSucceed(_ controller: RedeemVoucherViewController, with response: REST.SubmitVoucherResponse) {
- let controller = RedeemVoucherSucceededViewController(timeAddedComponents: response.dateComponents)
- controller.delegate = self
- navigationController.pushViewController(controller, animated: true)
- }
-
- func redeemVoucherDidCancel(_ controller: RedeemVoucherViewController) {
- didCancel?(self)
- }
-}
-
-extension AccountRedeemingVoucherCoordinator: RedeemVoucherSucceededViewControllerDelegate {
- func titleForAction(in controller: RedeemVoucherSucceededViewController) -> String {
- NSLocalizedString(
- "REDEEM_VOUCHER_DISMISS_BUTTON",
- tableName: "Welcome",
- value: "Next",
- comment: ""
+ let coordinator = AddCreditSucceededCoordinator(
+ purchaseType: .redeemingVoucher,
+ timeAdded: response.timeAdded,
+ navigationController: navigationController
)
- }
- func redeemVoucherSucceededViewControllerDidFinish(_ controller: RedeemVoucherSucceededViewController) {
- let coordinator = SetupAccountCompletedCoordinator(navigationController: navigationController)
- coordinator.didFinish = { [self] coordinator in
+ coordinator.didFinish = { [weak self] coordinator in
coordinator.removeFromParent()
+ guard let self else { return }
didFinish?(self)
}
+
addChild(coordinator)
- coordinator.start(animated: true)
+ coordinator.start()
+ }
+
+ func redeemVoucherDidCancel(_ controller: RedeemVoucherViewController) {
+ didCancel?(self)
}
}
diff --git a/ios/MullvadVPN/Coordinators/App/AddCreditSucceededCoordinator.swift b/ios/MullvadVPN/Coordinators/App/AddCreditSucceededCoordinator.swift
new file mode 100644
index 0000000000..40539338c0
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/AddCreditSucceededCoordinator.swift
@@ -0,0 +1,76 @@
+//
+// AddCreditSucceededCoordinator.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-08-01.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+final class AddCreditSucceededCoordinator: Coordinator {
+ var didFinish: ((AddCreditSucceededCoordinator) -> Void)?
+
+ let paymentType: PurchaseType
+ let timeAdded: Int
+ let navigationController: RootContainerViewController
+
+ enum PurchaseType {
+ case redeemingVoucher, inAppPurchase
+ }
+
+ init(purchaseType: PurchaseType, timeAdded: Int, navigationController: RootContainerViewController) {
+ self.timeAdded = timeAdded
+ self.navigationController = navigationController
+ self.paymentType = purchaseType
+ }
+
+ func start() {
+ let controller =
+ AddCreditSucceededViewController(timeAddedComponents: DateComponents(second: timeAdded))
+ controller.delegate = self
+ self.navigationController.pushViewController(controller, animated: true)
+ }
+}
+
+extension AddCreditSucceededCoordinator: AddCreditSucceededViewControllerDelegate {
+ func header(in controller: AddCreditSucceededViewController) -> String {
+ switch paymentType {
+ case .inAppPurchase:
+ return NSLocalizedString(
+ "IN_APP_PURCHASE_SUCCESS_TITLE",
+ tableName: "Welcome",
+ value: "Time was successfully added.",
+ comment: ""
+ )
+ case .redeemingVoucher:
+ return NSLocalizedString(
+ "REDEEM_VOUCHER_SUCCESS_TITLE",
+ tableName: "Welcome",
+ value: "Voucher was successfully redeemed.",
+ comment: ""
+ )
+ }
+ }
+
+ func titleForAction(in controller: AddCreditSucceededViewController) -> String {
+ NSLocalizedString(
+ "ADDED_TIME_SUCCESS_DISMISS_BUTTON",
+ tableName: "Welcome",
+ value: "Next",
+ comment: ""
+ )
+ }
+
+ func addCreditSucceededViewControllerDidFinish(in controller: AddCreditSucceededViewController) {
+ let coordinator = SetupAccountCompletedCoordinator(navigationController: navigationController)
+ coordinator.didFinish = { [weak self] coordinator in
+ coordinator.removeFromParent()
+ guard let self else { return }
+ didFinish?(self)
+ }
+ addChild(coordinator)
+ coordinator.start(animated: true)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
index b8d89f4831..ce28210be9 100644
--- a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
@@ -46,7 +46,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
preferredContentSize: UIMetrics.preferredFormSheetContentSize,
modalPresentationStyle: .custom,
isModalInPresentation: true,
- transitioningDelegate: SecondaryContextTransitioningDelegate()
+ transitioningDelegate: SecondaryContextTransitioningDelegate(adjustViewWhenKeyboardAppears: false)
)
private let notificationController = NotificationController()
@@ -70,6 +70,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
private let apiProxy: REST.APIProxy
private let devicesProxy: REST.DevicesProxy
private var tunnelObserver: TunnelObserver?
+ private var appPreferences: AppPreferencesDataSource
private var outOfTimeTimer: Timer?
@@ -82,13 +83,15 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
storePaymentManager: StorePaymentManager,
relayCacheTracker: RelayCacheTracker,
apiProxy: REST.APIProxy,
- devicesProxy: REST.DevicesProxy
+ devicesProxy: REST.DevicesProxy,
+ appPreferences: AppPreferencesDataSource
) {
self.tunnelManager = tunnelManager
self.storePaymentManager = storePaymentManager
self.relayCacheTracker = relayCacheTracker
self.apiProxy = apiProxy
self.devicesProxy = devicesProxy
+ self.appPreferences = appPreferences
super.init()
@@ -150,9 +153,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
case .welcome:
presentWelcome(animated: animated, completion: completion)
-
- case .setupAccountCompleted:
- presentSetupAccountCompleted(animated: animated, completion: completion)
}
}
@@ -303,7 +303,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
*/
private func evaluateNextRoutes() -> [AppRoute] {
// Show TOS alone blocking all other routes.
- guard TermsOfService.isAgreed else {
+ guard appPreferences.isAgreedToTermsOfService else {
return [.tos]
}
@@ -318,15 +318,15 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
routes.append(.login)
case let .loggedIn(accountData, _):
- if accountData.isExpired {
- routes.append(accountData.isNew ? .welcome : .outOfTime)
+ if !appPreferences.isShownOnboarding {
+ routes.append(.welcome)
} else {
- routes.append(.main)
+ routes.append(accountData.isExpired ? .outOfTime : .main)
}
}
- // Changelog can be presented simultaneously with other routes.
- if !ChangeLog.isSeen {
+ // Change log can be presented simultaneously with other routes.
+ if !appPreferences.isSeenLatestChanges {
routes.append(.changelog)
}
@@ -501,6 +501,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
let coordinator = TermsOfServiceCoordinator(navigationController: horizontalFlowController)
coordinator.didFinish = { [weak self] coordinator in
+ self?.appPreferences.isAgreedToTermsOfService = true
self?.continueFlow(animated: true)
}
@@ -513,12 +514,11 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
}
private func presentChangeLog(animated: Bool, completion: @escaping (Coordinator) -> Void) {
- let coordinator = ChangeLogCoordinator()
-
- coordinator.didFinish = { [weak self] in
- ChangeLog.markAsSeen()
+ let coordinator = ChangeLogCoordinator(interactor: ChangeLogInteractor())
- self?.router.dismiss(.changelog, animated: true)
+ coordinator.didFinish = { [weak self] coordinator in
+ self?.appPreferences.markChangeLogSeen()
+ self?.router.dismiss(.changelog)
}
coordinator.start()
@@ -595,26 +595,10 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
tunnelManager: tunnelManager
)
- coordinator.didFinishPayment = { [weak self] coordinator in
- guard let self else { return }
- router.dismiss(.welcome, animated: false)
- continueFlow(animated: false)
- }
-
- addChild(coordinator)
- coordinator.start(animated: animated)
-
- beginHorizontalFlow(animated: animated) {
- completion(coordinator)
- }
- }
-
- private func presentSetupAccountCompleted(animated: Bool, completion: @escaping (Coordinator) -> Void) {
- let coordinator = SetupAccountCompletedCoordinator(navigationController: horizontalFlowController)
-
coordinator.didFinish = { [weak self] coordinator in
guard let self else { return }
- coordinator.removeFromParent()
+ appPreferences.isShownOnboarding = true
+ router.dismiss(.welcome, animated: false)
continueFlow(animated: false)
}
@@ -652,6 +636,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
coordinator.didFinish = { [weak self] coordinator in
self?.continueFlow(animated: true)
}
+ coordinator.didCreateAccount = { [weak self] in
+ self?.appPreferences.isShownOnboarding = false
+ }
addChild(coordinator)
coordinator.start(animated: animated)
@@ -706,13 +693,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
self?.didDismissAccount(reason)
}
- coordinator.didAddMoreCredit = { [weak self] coordinator, option in
- guard let self,
- self.isPresentingWelcome else { return }
- self.router.dismiss(.welcome, animated: false)
- self.router.present(.setupAccountCompleted, animated: false)
- }
-
coordinator.start(animated: animated)
presentChild(
@@ -720,7 +700,11 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
animated: animated,
configuration: ModalPresentationConfiguration(
preferredContentSize: UIMetrics.preferredFormSheetContentSize,
- modalPresentationStyle: .formSheet
+ modalPresentationStyle: .custom,
+ transitioningDelegate: FormSheetTransitioningDelegate(options: FormSheetPresentationOptions(
+ useFullScreenPresentationInCompactWidth: true,
+ adjustViewWhenKeyboardAppears: false
+ ))
)
) { [weak self] in
completion(coordinator)
@@ -794,7 +778,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
case let .loggedIn(accountData, _):
// Account creation is being shown
- guard !isPresentingWelcome && !accountData.isNew else { return }
+ guard !isPresentingWelcome && !appPreferences.isShownOnboarding else { return }
// Handle transition to and from expired state.
switch (previousDeviceState.accountData?.isExpired ?? false, accountData.isExpired) {
@@ -811,9 +795,11 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
break
}
case .revoked:
+ appPreferences.isShownOnboarding = true
cancelOutOfTimeTimer()
router.present(.revoked, animated: true)
case .loggedOut:
+ appPreferences.isShownOnboarding = true
cancelOutOfTimeTimer()
}
}
@@ -983,3 +969,13 @@ extension DeviceState {
isLoggedIn ? UISplitViewController.DisplayMode.oneBesideSecondary : .secondaryOnly
}
}
+
+fileprivate extension AppPreferencesDataSource {
+ var isSeenLatestChanges: Bool {
+ self.lastSeenChangeLogVersion == Bundle.main.shortVersion
+ }
+
+ mutating func markChangeLogSeen() {
+ self.lastSeenChangeLogVersion = Bundle.main.shortVersion
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/ChangeLogCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ChangeLogCoordinator.swift
index fe42cd9fbe..90756c68ce 100644
--- a/ios/MullvadVPN/Coordinators/App/ChangeLogCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/ChangeLogCoordinator.swift
@@ -10,29 +10,26 @@ import MullvadLogging
import UIKit
final class ChangeLogCoordinator: Coordinator, Presentable {
- private let logger = Logger(label: "ChangeLogCoordinator")
-
private var alertController: CustomAlertViewController?
+ private let interactor: ChangeLogInteractor
+ var didFinish: ((ChangeLogCoordinator) -> Void)?
var presentedViewController: UIViewController {
return alertController!
}
- var didFinish: (() -> Void)?
+ init(interactor: ChangeLogInteractor) {
+ self.interactor = interactor
+ }
func start() {
- alertController = CustomAlertViewController(
- header: Bundle.main.shortVersion,
- title: NSLocalizedString(
- "CHANGE_LOG_TITLE",
- tableName: "Account",
- value: "Changes in this version:",
- comment: ""
- ),
- attributedMessage: readChangeLogFromFile()
+ let alertController = CustomAlertViewController(
+ header: interactor.viewModel.header,
+ title: interactor.viewModel.title,
+ attributedMessage: interactor.viewModel.body
)
- alertController?.addAction(
+ alertController.addAction(
title: NSLocalizedString(
"CHANGE_LOG_OK_ACTION",
tableName: "Account",
@@ -41,35 +38,10 @@ final class ChangeLogCoordinator: Coordinator, Presentable {
),
style: .default,
handler: { [weak self] in
- self?.didFinish?()
+ guard let self else { return }
+ didFinish?(self)
}
)
- }
-
- private func readChangeLogFromFile() -> NSAttributedString? {
- guard let changeLogText = try? ChangeLog.readFromFile() else {
- logger.error("Cannot read changelog from bundle.")
- return nil
- }
-
- let bullet = "• "
- let font = UIFont.preferredFont(forTextStyle: .body)
-
- let bulletList = changeLogText.split(whereSeparator: { $0.isNewline })
- .map { "\(bullet)\($0)" }
- .joined(separator: "\n")
-
- let paragraphStyle = NSMutableParagraphStyle()
- paragraphStyle.lineBreakMode = .byWordWrapping
- paragraphStyle.headIndent = bullet.size(withAttributes: [.font: font]).width
-
- return NSAttributedString(
- string: bulletList,
- attributes: [
- .paragraphStyle: paragraphStyle,
- .font: font,
- .foregroundColor: UIColor.white.withAlphaComponent(0.8),
- ]
- )
+ self.alertController = alertController
}
}
diff --git a/ios/MullvadVPN/Coordinators/App/InAppPurchaseCoordinator.swift b/ios/MullvadVPN/Coordinators/App/InAppPurchaseCoordinator.swift
new file mode 100644
index 0000000000..8519ddab04
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/InAppPurchaseCoordinator.swift
@@ -0,0 +1,71 @@
+//
+// InAppPurchaseCoordinator.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-07-21.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import StoreKit
+import UIKit
+
+class InAppPurchaseCoordinator: Coordinator, Presentable {
+ private let navigationController: RootContainerViewController
+ private let interactor: InAppPurchaseInteractor
+
+ var didFinish: ((InAppPurchaseCoordinator) -> Void)?
+ var didCancel: ((InAppPurchaseCoordinator) -> Void)?
+
+ var presentedViewController: UIViewController {
+ navigationController
+ }
+
+ init(navigationController: RootContainerViewController, interactor: InAppPurchaseInteractor) {
+ self.navigationController = navigationController
+ self.interactor = interactor
+ }
+
+ func start(accountNumber: String, product: SKProduct) {
+ interactor.purchase(accountNumber: accountNumber, product: product)
+ interactor.didFinishPayment = { [weak self] interactor, paymentEvent in
+ guard let self else { return }
+ switch paymentEvent {
+ case let .finished(value):
+ let coordinator = AddCreditSucceededCoordinator(
+ purchaseType: .inAppPurchase,
+ timeAdded: Int(value.serverResponse.timeAdded),
+ navigationController: navigationController
+ )
+
+ coordinator.didFinish = { [weak self] coordinator in
+ coordinator.removeFromParent()
+ guard let self else { return }
+ didFinish?(self)
+ }
+
+ addChild(coordinator)
+ coordinator.start()
+
+ case let .failure(failure):
+ let alertController = CustomAlertViewController(
+ message: failure.error.localizedDescription,
+ icon: .alert
+ )
+
+ 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)
+ }
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/LoginCoordinator.swift b/ios/MullvadVPN/Coordinators/App/LoginCoordinator.swift
index 02946e1d53..a659fa5020 100644
--- a/ios/MullvadVPN/Coordinators/App/LoginCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/LoginCoordinator.swift
@@ -19,6 +19,7 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat
private var lastLoginAction: LoginAction?
var didFinish: ((LoginCoordinator) -> Void)?
+ var didCreateAccount: (() -> Void)?
let navigationController: RootContainerViewController
@@ -39,6 +40,7 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat
loginController.didFinishLogin = { [weak self] action, error in
self?.didFinishLogin(action: action, error: error) ?? .nothing
}
+ interactor.didCreateAccount = self.didCreateAccount
navigationController.pushViewController(loginController, animated: animated)
diff --git a/ios/MullvadVPN/Coordinators/App/OutOfTimeCoordinator.swift b/ios/MullvadVPN/Coordinators/App/OutOfTimeCoordinator.swift
index fa7ccd5ff2..5613b8bc6c 100644
--- a/ios/MullvadVPN/Coordinators/App/OutOfTimeCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/OutOfTimeCoordinator.swift
@@ -33,6 +33,12 @@ class OutOfTimeCoordinator: Coordinator, OutOfTimeViewControllerDelegate {
storePaymentManager: storePaymentManager,
tunnelManager: tunnelManager
)
+
+ interactor.didAddMoreCredit = { [weak self] in
+ guard let self else { return }
+ didFinishPayment?(self)
+ }
+
let controller = OutOfTimeViewController(
interactor: interactor,
errorPresenter: PaymentAlertPresenter(
diff --git a/ios/MullvadVPN/Coordinators/App/SettingsRedeemVoucherCoordinator.swift b/ios/MullvadVPN/Coordinators/App/SettingsRedeemVoucherCoordinator.swift
index 9fa3de6f81..37fb0fbfe2 100644
--- a/ios/MullvadVPN/Coordinators/App/SettingsRedeemVoucherCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/SettingsRedeemVoucherCoordinator.swift
@@ -40,7 +40,7 @@ extension SettingsRedeemVoucherCoordinator: RedeemVoucherViewControllerDelegate
_ controller: RedeemVoucherViewController,
with response: REST.SubmitVoucherResponse
) {
- let viewController = RedeemVoucherSucceededViewController(timeAddedComponents: response.dateComponents)
+ let viewController = AddCreditSucceededViewController(timeAddedComponents: response.dateComponents)
viewController.delegate = self
navigationController.pushViewController(viewController, animated: true)
}
@@ -50,17 +50,26 @@ extension SettingsRedeemVoucherCoordinator: RedeemVoucherViewControllerDelegate
}
}
-extension SettingsRedeemVoucherCoordinator: RedeemVoucherSucceededViewControllerDelegate {
- func titleForAction(in controller: RedeemVoucherSucceededViewController) -> String {
+extension SettingsRedeemVoucherCoordinator: AddCreditSucceededViewControllerDelegate {
+ func addCreditSucceededViewControllerDidFinish(in controller: AddCreditSucceededViewController) {
+ didFinish?(self)
+ }
+
+ func header(in controller: AddCreditSucceededViewController) -> String {
NSLocalizedString(
- "REDEEM_VOUCHER_DISMISS_BUTTON",
+ "REDEEM_VOUCHER_SUCCESS_TITLE",
tableName: "RedeemVoucher",
- value: "Got it!",
+ value: "Voucher was successfully redeemed.",
comment: ""
)
}
- func redeemVoucherSucceededViewControllerDidFinish(_ controller: RedeemVoucherSucceededViewController) {
- didFinish?(self)
+ func titleForAction(in controller: AddCreditSucceededViewController) -> String {
+ NSLocalizedString(
+ "REDEEM_VOUCHER_DISMISS_BUTTON",
+ tableName: "RedeemVoucher",
+ value: "Got it!",
+ comment: ""
+ )
}
}
diff --git a/ios/MullvadVPN/Coordinators/App/SetupAccountCompletedCoordinator.swift b/ios/MullvadVPN/Coordinators/App/SetupAccountCompletedCoordinator.swift
index 262336e287..e2bbe3d42b 100644
--- a/ios/MullvadVPN/Coordinators/App/SetupAccountCompletedCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/SetupAccountCompletedCoordinator.swift
@@ -11,7 +11,7 @@ import UIKit
class SetupAccountCompletedCoordinator: Coordinator, Presenting {
private let navigationController: RootContainerViewController
- private var viewController: SetupAccountCompletedViewController?
+ private var viewController: SetupAccountCompletedController?
var didFinish: ((SetupAccountCompletedCoordinator) -> Void)?
@@ -24,7 +24,7 @@ class SetupAccountCompletedCoordinator: Coordinator, Presenting {
}
func start(animated: Bool) {
- let controller = SetupAccountCompletedViewController()
+ let controller = SetupAccountCompletedController()
controller.delegate = self
viewController = controller
@@ -33,12 +33,12 @@ class SetupAccountCompletedCoordinator: Coordinator, Presenting {
}
}
-extension SetupAccountCompletedCoordinator: SetupAccountCompletedViewControllerDelegate {
- func didRequestToSeePrivacy(controller: SetupAccountCompletedViewController) {
+extension SetupAccountCompletedCoordinator: SetupAccountCompletedControllerDelegate {
+ func didRequestToSeePrivacy(controller: SetupAccountCompletedController) {
presentChild(SafariCoordinator(url: ApplicationConfiguration.privacyGuidesURL), animated: true)
}
- func didRequestToStartTheApp(controller: SetupAccountCompletedViewController) {
+ func didRequestToStartTheApp(controller: SetupAccountCompletedController) {
didFinish?(self)
}
}
diff --git a/ios/MullvadVPN/Coordinators/App/TermsOfServiceCoordinator.swift b/ios/MullvadVPN/Coordinators/App/TermsOfServiceCoordinator.swift
index c8f9574e33..5d81981b08 100644
--- a/ios/MullvadVPN/Coordinators/App/TermsOfServiceCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/TermsOfServiceCoordinator.swift
@@ -30,9 +30,6 @@ class TermsOfServiceCoordinator: Coordinator, Presenting {
controller.completionHandler = { [weak self] in
guard let self else { return }
-
- TermsOfService.setAgreed()
-
didFinish?(self)
}
diff --git a/ios/MullvadVPN/Coordinators/App/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/App/WelcomeCoordinator.swift
index e458088f0a..7146155586 100644
--- a/ios/MullvadVPN/Coordinators/App/WelcomeCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/WelcomeCoordinator.swift
@@ -8,20 +8,27 @@
import Foundation
import MullvadREST
+import StoreKit
import UIKit
-final class WelcomeCoordinator: Coordinator, Presentable {
+final class WelcomeCoordinator: Coordinator, Presentable, Presenting {
private let navigationController: RootContainerViewController
private let storePaymentManager: StorePaymentManager
private let tunnelManager: TunnelManager
+ private let inAppPurchaseInteractor: InAppPurchaseInteractor
+
private var viewController: WelcomeViewController?
- var didFinishPayment: ((WelcomeCoordinator) -> Void)?
+ var didFinish: ((WelcomeCoordinator) -> Void)?
var presentedViewController: UIViewController {
navigationController
}
+ var presentationContext: UIViewController {
+ navigationController
+ }
+
init(
navigationController: RootContainerViewController,
storePaymentManager: StorePaymentManager,
@@ -30,18 +37,27 @@ final class WelcomeCoordinator: Coordinator, Presentable {
self.navigationController = navigationController
self.storePaymentManager = storePaymentManager
self.tunnelManager = tunnelManager
+ self.inAppPurchaseInteractor = InAppPurchaseInteractor(storePaymentManager: storePaymentManager)
}
func start(animated: Bool) {
- guard case let .loggedIn(storedAccountData, storedDeviceData) = tunnelManager.deviceState else {
- return
- }
let interactor = WelcomeInteractor(
- deviceData: storedDeviceData,
- accountData: storedAccountData,
+ storePaymentManager: storePaymentManager,
tunnelManager: tunnelManager
)
+ interactor.didAddMoreCredit = { [weak self] in
+ guard let self else { return }
+ let coordinator = SetupAccountCompletedCoordinator(navigationController: navigationController)
+ coordinator.didFinish = { [weak self] coordinator in
+ coordinator.removeFromParent()
+ guard let self else { return }
+ didFinish?(self)
+ }
+ addChild(coordinator)
+ coordinator.start(animated: true)
+ }
+
let controller = WelcomeViewController(interactor: interactor)
controller.delegate = self
@@ -99,8 +115,27 @@ extension WelcomeCoordinator: WelcomeViewControllerDelegate {
presentedViewController.present(alertController, animated: true)
}
- func didRequestToPurchaseCredit(controller: WelcomeViewController) {
- // TODO: In-app purchase
+ func didRequestToPurchaseCredit(controller: WelcomeViewController, accountNumber: String, product: SKProduct) {
+ let coordinator = InAppPurchaseCoordinator(
+ navigationController: navigationController,
+ interactor: inAppPurchaseInteractor
+ )
+
+ inAppPurchaseInteractor.viewControllerDelegate = viewController
+
+ coordinator.didFinish = { [weak self] coordinator in
+ guard let self else { return }
+ coordinator.removeFromParent()
+ didFinish?(self)
+ }
+
+ coordinator.didCancel = { coordinator in
+ coordinator.removeFromParent()
+ }
+
+ addChild(coordinator)
+
+ coordinator.start(accountNumber: accountNumber, product: product)
}
func didRequestToRedeemVoucher(controller: WelcomeViewController) {
@@ -116,19 +151,13 @@ extension WelcomeCoordinator: WelcomeViewControllerDelegate {
}
coordinator.didFinish = { [weak self] coordinator in
- guard let self else { return }
coordinator.removeFromParent()
- didFinishPayment?(self)
+ guard let self else { return }
+ didFinish?(self)
}
addChild(coordinator)
coordinator.start()
}
-
- func didUpdateDeviceState(deviceState: DeviceState) {
- if deviceState.accountData?.isExpired == false {
- didFinishPayment?(self)
- }
- }
}
diff --git a/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift b/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift
index e472aa19ea..8a91fa6264 100644
--- a/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift
+++ b/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift
@@ -7,6 +7,17 @@
//
import UIKit
+struct FormSheetPresentationOptions {
+ /**
+ Indicates whether the presentation controller should use a fullscreen presentation when in a compact width environment
+ */
+ var useFullScreenPresentationInCompactWidth = false
+
+ /**
+ Indicates whether the presentation controller should handle keyboard notifications
+ */
+ var adjustViewWhenKeyboardAppears = false
+}
/**
Custom implementation of a formsheet presentation controller.
@@ -29,6 +40,11 @@ class FormSheetPresentationController: UIPresentationController {
*/
private var lastKnownIsInFullScreen: Bool?
+ /**
+ Change the position of `presentedView` if `FormSheetPresentationOptions.adjustViewWhenKeyboardAppears` is `true`
+ */
+ private var keyboardResponder: AutomaticKeyboardResponder?
+
private let dimmingView: UIView = {
let dimmingView = UIView()
dimmingView.backgroundColor = UIMetrics.DimmingView.backgroundColor
@@ -40,19 +56,25 @@ class FormSheetPresentationController: UIPresentationController {
}
/**
- Flag indicating whether presentation controller should use fullscreen presentation when in
- compact width environment
- */
- var useFullScreenPresentationInCompactWidth = false
-
- /**
Returns `true` if presentation controller is in fullscreen presentation.
*/
var isInFullScreenPresentation: Bool {
- useFullScreenPresentationInCompactWidth &&
+ options.useFullScreenPresentationInCompactWidth &&
traitCollection.horizontalSizeClass == .compact
}
+ private let options: FormSheetPresentationOptions
+
+ init(
+ presentedViewController: UIViewController,
+ presenting presentingViewController: UIViewController?,
+ options: FormSheetPresentationOptions
+ ) {
+ self.options = options
+ super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
+ addKeyboardResponderIfNeeded()
+ }
+
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView else {
return super.frameOfPresentedViewInContainerView
@@ -147,9 +169,39 @@ class FormSheetPresentationController: UIPresentationController {
userInfo: [Self.isFullScreenUserInfoKey: NSNumber(booleanLiteral: currentIsInFullScreen)]
)
}
+
+ private func addKeyboardResponderIfNeeded() {
+ guard options.adjustViewWhenKeyboardAppears,
+ let presentedView else { return }
+ keyboardResponder = AutomaticKeyboardResponder(
+ targetView: presentedView,
+ handler: { [weak self] view, adjustment in
+ guard let self,
+ let containerView,
+ !isInFullScreenPresentation else { return }
+ let frame = view.frame
+ let bottomMarginFromKeyboard = adjustment > 0 ? UIMetrics.sectionSpacing : 0
+ view.frame = CGRect(
+ origin: CGPoint(
+ x: frame.origin.x,
+ y: containerView.bounds.midY - presentedViewController.preferredContentSize
+ .height * 0.5 - adjustment - bottomMarginFromKeyboard
+ ),
+ size: frame.size
+ )
+ view.layoutIfNeeded()
+ }
+ )
+ }
}
class FormSheetTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
+ let options: FormSheetPresentationOptions
+
+ init(options: FormSheetPresentationOptions = FormSheetPresentationOptions()) {
+ self.options = options
+ }
+
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
@@ -170,7 +222,8 @@ class FormSheetTransitioningDelegate: NSObject, UIViewControllerTransitioningDel
) -> UIPresentationController? {
FormSheetPresentationController(
presentedViewController: presented,
- presenting: source
+ presenting: source,
+ options: options
)
}
}
diff --git a/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift b/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift
index 021e5ef76e..f218104074 100644
--- a/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift
+++ b/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift
@@ -49,6 +49,18 @@ class SecondaryContextPresentationController: FormSheetPresentationController {
}
class SecondaryContextTransitioningDelegate: FormSheetTransitioningDelegate {
+ convenience init(adjustViewWhenKeyboardAppears: Bool) {
+ let option = FormSheetPresentationOptions(
+ useFullScreenPresentationInCompactWidth: true,
+ adjustViewWhenKeyboardAppears: adjustViewWhenKeyboardAppears
+ )
+ self.init(options: option)
+ }
+
+ private override init(options: FormSheetPresentationOptions) {
+ super.init(options: options)
+ }
+
override func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
@@ -56,11 +68,10 @@ class SecondaryContextTransitioningDelegate: FormSheetTransitioningDelegate {
) -> UIPresentationController? {
let presentationController = SecondaryContextPresentationController(
presentedViewController: presented,
- presenting: source
+ presenting: source,
+ options: options
)
- presentationController.useFullScreenPresentationInCompactWidth = true
-
return presentationController
}
}
diff --git a/ios/MullvadVPN/Routing/AppRoutes.swift b/ios/MullvadVPN/Routing/AppRoutes.swift
index 4fda9aa253..9373c53b3f 100644
--- a/ios/MullvadVPN/Routing/AppRoutes.swift
+++ b/ios/MullvadVPN/Routing/AppRoutes.swift
@@ -85,7 +85,7 @@ enum AppRoute: AppRouteProtocol {
/**
Routes that are part of primary horizontal navigation group.
*/
- case tos, login, main, revoked, outOfTime, welcome, setupAccountCompleted
+ case tos, login, main, revoked, outOfTime, welcome
var isExclusive: Bool {
switch self {
@@ -106,7 +106,7 @@ enum AppRoute: AppRouteProtocol {
var routeGroup: AppRouteGroup {
switch self {
- case .tos, .login, .main, .revoked, .outOfTime, .welcome, .setupAccountCompleted:
+ case .tos, .login, .main, .revoked, .outOfTime, .welcome:
return .primary
case .changelog:
return .changelog
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index e625fc8200..a80dd85953 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -65,7 +65,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand
storePaymentManager: appDelegate.storePaymentManager,
relayCacheTracker: appDelegate.relayCacheTracker,
apiProxy: appDelegate.apiProxy,
- devicesProxy: appDelegate.devicesProxy
+ devicesProxy: appDelegate.devicesProxy,
+ appPreferences: AppPreferences()
)
appCoordinator?.onShowSettings = { [weak self] in
diff --git a/ios/MullvadVPN/SettingsManager/StoredAccountData.swift b/ios/MullvadVPN/SettingsManager/StoredAccountData.swift
index fac43b6ec2..4870c3018f 100644
--- a/ios/MullvadVPN/SettingsManager/StoredAccountData.swift
+++ b/ios/MullvadVPN/SettingsManager/StoredAccountData.swift
@@ -18,9 +18,6 @@ struct StoredAccountData: Codable, Equatable {
/// Account expiry.
var expiry: Date
- /// Set to `true` when the account is created and flipped to `false` when the user adds more credit.
- var isNew = false
-
/// Returns `true` if account has expired.
var isExpired: Bool {
expiry <= Date()
@@ -33,10 +30,5 @@ extension StoredAccountData {
self.identifier = try container.decode(String.self, forKey: .identifier)
self.number = try container.decode(String.self, forKey: .number)
self.expiry = try container.decode(Date.self, forKey: .expiry)
-
- // When the app is upgraded from 2023.3 or below, this field won't exist, and the auto synthesized init will fail.
- // This leads to a reset of the settings. If the key isn't present, consider the account not new to avoid the issue.
- let isNewAccount = try? container.decode(Bool.self, forKey: .isNew)
- self.isNew = isNewAccount ?? false
}
}
diff --git a/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift b/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift
index 3af92bc6de..d87c61b37e 100644
--- a/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift
@@ -67,9 +67,6 @@ class RedeemVoucherOperation: ResultOperation<REST.SubmitVoucherResponse> {
case .loggedIn(var storedAccountData, let storedDeviceData):
storedAccountData.expiry = voucherResponse.newExpiry
- // flip the value to `false` when adding credit is successful
- storedAccountData.isNew = false
-
let newDeviceState = DeviceState.loggedIn(storedAccountData, storedDeviceData)
interactor.setDeviceState(newDeviceState, persist: true)
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index 0ae6e8b5e6..ae9425cbac 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -226,8 +226,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> {
return StoredAccountData(
identifier: newAccountData.id,
number: newAccountData.number,
- expiry: newAccountData.expiry,
- isNew: true
+ expiry: newAccountData.expiry
)
}
diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift
index 7ae20ade94..6595a410fd 100644
--- a/ios/MullvadVPN/UI appearance/UIMetrics.swift
+++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift
@@ -36,7 +36,7 @@ enum UIMetrics {
enum SettingsRedeemVoucher {
static let cornerRadius = 8.0
- static let preferredContentSize = CGSize(width: 280, height: 240)
+ static let preferredContentSize = CGSize(width: 280, height: 260)
static let contentLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0)
}
diff --git a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogInteractor.swift b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogInteractor.swift
new file mode 100644
index 0000000000..3e5a85007f
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogInteractor.swift
@@ -0,0 +1,54 @@
+//
+// ChangeLogInteractor.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-08-11.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+
+final class ChangeLogInteractor {
+ private let logger = Logger(label: "ChangeLogInteractor")
+ private var items: [String] = []
+ var viewModel: ChangeLogViewModel {
+ return ChangeLogViewModel(
+ body: items
+ )
+ }
+
+ init() {
+ do {
+ let string = try readFromFile()
+ items = string.split(whereSeparator: { $0.isNewline }).map { String($0) }
+ } catch {
+ logger.error(error: error, message: "Cannot read change log from bundle.")
+ }
+ }
+
+ /**
+ Reads change log file from bundle and returns its contents as a string.
+ */
+ private func readFromFile() throws -> String {
+ try String(contentsOfFile: try getPathToChangesFile())
+ .split(whereSeparator: { $0.isNewline })
+ .compactMap { line in
+ let trimmedString = line.trimmingCharacters(in: .whitespaces)
+
+ return trimmedString.isEmpty ? nil : trimmedString
+ }
+ .joined(separator: "\n")
+ }
+
+ /**
+ Returns path to change log file in bundle.
+ */
+ private func getPathToChangesFile() throws -> String {
+ if let filePath = Bundle.main.path(forResource: "changes", ofType: "txt") {
+ return filePath
+ } else {
+ throw CocoaError(.fileNoSuchFile)
+ }
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogViewModel.swift b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogViewModel.swift
new file mode 100644
index 0000000000..ca45c5d3c1
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogViewModel.swift
@@ -0,0 +1,48 @@
+//
+// ChangeLogViewModel.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-08-22.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import UIKit.NSAttributedString
+import UIKit.UIFont
+
+struct ChangeLogViewModel {
+ let header: String = Bundle.main.shortVersion
+ let title: String = NSLocalizedString(
+ "CHANGE_LOG_TITLE",
+ tableName: "Account",
+ value: "Changes in this version:",
+ comment: ""
+ )
+ let body: NSAttributedString
+
+ init(body: [String]) {
+ self.body = body.changeLogAttributedString
+ }
+}
+
+fileprivate extension Array where Element == String {
+ var changeLogAttributedString: NSAttributedString {
+ let bullet = "• "
+ let font = UIFont.preferredFont(forTextStyle: .body)
+ let paragraphStyle = NSMutableParagraphStyle()
+ paragraphStyle.lineBreakMode = .byWordWrapping
+ paragraphStyle.headIndent = bullet.size(withAttributes: [.font: font]).width
+
+ return NSAttributedString(
+ string: self.map {
+ "\(bullet)\($0)"
+ }
+ .joined(separator: "\n"),
+ attributes: [
+ .paragraphStyle: paragraphStyle,
+ .font: font,
+ .foregroundColor: UIColor.white.withAlphaComponent(0.8),
+ ]
+ )
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedViewController.swift b/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedController.swift
index ad6dcadb54..a7f4cb91ea 100644
--- a/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedViewController.swift
+++ b/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedController.swift
@@ -1,5 +1,5 @@
//
-// SetupAccountCompletedViewController.swift
+// SetupAccountCompletedController.swift
// MullvadVPN
//
// Created by Mojgan on 2023-06-30.
@@ -8,19 +8,35 @@
import UIKit
-protocol SetupAccountCompletedViewControllerDelegate: AnyObject {
- func didRequestToSeePrivacy(controller: SetupAccountCompletedViewController)
- func didRequestToStartTheApp(controller: SetupAccountCompletedViewController)
+protocol SetupAccountCompletedControllerDelegate: AnyObject {
+ func didRequestToSeePrivacy(controller: SetupAccountCompletedController)
+ func didRequestToStartTheApp(controller: SetupAccountCompletedController)
}
-class SetupAccountCompletedViewController: UIViewController {
+class SetupAccountCompletedController: UIViewController, RootContainment {
private lazy var contentView: SetupAccountCompletedContentView = {
let view = SetupAccountCompletedContentView()
view.delegate = self
return view
}()
- weak var delegate: SetupAccountCompletedViewControllerDelegate?
+ var preferredHeaderBarPresentation: HeaderBarPresentation {
+ HeaderBarPresentation(style: .default, showsDivider: true)
+ }
+
+ var prefersHeaderBarHidden: Bool {
+ false
+ }
+
+ var prefersDeviceInfoBarHidden: Bool {
+ true
+ }
+
+ var prefersNotificationBarHidden: Bool {
+ true
+ }
+
+ weak var delegate: SetupAccountCompletedControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
@@ -35,7 +51,7 @@ class SetupAccountCompletedViewController: UIViewController {
}
}
-extension SetupAccountCompletedViewController: SetupAccountCompletedContentViewDelegate {
+extension SetupAccountCompletedController: SetupAccountCompletedContentViewDelegate {
func didTapPrivacyButton(view: SetupAccountCompletedContentView, button: AppButton) {
delegate?.didRequestToSeePrivacy(controller: self)
}
diff --git a/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift b/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift
new file mode 100644
index 0000000000..567df8fc89
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift
@@ -0,0 +1,46 @@
+//
+// InAppPurchaseInteractor.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-07-21.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import StoreKit
+
+protocol InAppPurchaseViewControllerDelegate: AnyObject {
+ func didBeginPayment()
+ func didEndPayment()
+}
+
+class InAppPurchaseInteractor {
+ let storePaymentManager: StorePaymentManager
+ var didFinishPayment: ((InAppPurchaseInteractor, StorePaymentEvent) -> Void)?
+ weak var viewControllerDelegate: InAppPurchaseViewControllerDelegate?
+
+ private var paymentObserver: StorePaymentObserver?
+
+ init(storePaymentManager: StorePaymentManager) {
+ self.storePaymentManager = storePaymentManager
+ self.addObservers()
+ }
+
+ private func addObservers() {
+ let paymentObserver = StorePaymentBlockObserver { [weak self] _, event in
+ guard let self else { return }
+ viewControllerDelegate?.didEndPayment()
+ didFinishPayment?(self, event)
+ }
+
+ storePaymentManager.addPaymentObserver(paymentObserver)
+
+ self.paymentObserver = paymentObserver
+ }
+
+ func purchase(accountNumber: String, product: SKProduct) {
+ let payment = SKPayment(product: product)
+ storePaymentManager.addPayment(payment, for: accountNumber)
+ viewControllerDelegate?.didBeginPayment()
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift
index 3154a0f3d5..479c096aac 100644
--- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift
+++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift
@@ -7,6 +7,7 @@
//
import UIKit
+
protocol WelcomeContentViewDelegate: AnyObject {
func didTapPurchaseButton(welcomeContentView: WelcomeContentView, button: AppButton)
func didTapRedeemVoucherButton(welcomeContentView: WelcomeContentView, button: AppButton)
@@ -71,9 +72,8 @@ final class WelcomeContentView: UIView {
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body)
label.textColor = .white
- label.lineBreakMode = .byWordWrapping
- label.numberOfLines = .zero
- label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ label.setContentHuggingPriority(.defaultLow, for: .horizontal)
+ label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}()
@@ -83,7 +83,8 @@ final class WelcomeContentView: UIView {
button.tintColor = .white
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "IconInfo"), for: .normal)
- button.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+ button.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ button.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
return button
}()
@@ -118,7 +119,6 @@ final class WelcomeContentView: UIView {
comment: ""
)
button.setTitle(localizedString, for: .normal)
- button.setImage(UIImage(named: "IconExtlink")?.imageFlippedForRightToLeftLayoutDirection(), for: .normal)
return button
}()
@@ -149,8 +149,8 @@ final class WelcomeContentView: UIView {
private let spacerView: UIView = {
let view = UIView()
- view.setContentHuggingPriority(.fittingSizeLevel, for: .horizontal)
- view.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
+ view.setContentHuggingPriority(.required, for: .horizontal)
+ view.setContentCompressionResistancePriority(.required, for: .horizontal)
return view
}()
@@ -174,6 +174,24 @@ final class WelcomeContentView: UIView {
}
}
+ var isPurchasing = false {
+ didSet {
+ let alpha = isPurchasing ? 0.7 : 1.0
+ purchaseButton.isLoading = isPurchasing
+ purchaseButton.alpha = alpha
+ redeemVoucherButton.isEnabled = !isPurchasing
+ redeemVoucherButton.alpha = alpha
+ }
+ }
+
+ var productState: ProductState = .none {
+ didSet {
+ purchaseButton.setTitle(productState.purchaseButtonTitle, for: .normal)
+ purchaseButton.isLoading = productState.isFetching
+ purchaseButton.isEnabled = productState.isReceived
+ }
+ }
+
override init(frame: CGRect) {
super.init(frame: frame)
diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift
index 0535c84384..7c6356a3ca 100644
--- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift
+++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift
@@ -8,43 +8,91 @@
import Foundation
import MullvadLogging
+import StoreKit
-class WelcomeInteractor {
- private let logger = Logger(label: "WelcomeInteractor")
+final class WelcomeInteractor {
+ private let storePaymentManager: StorePaymentManager
+ private let tunnelManager: TunnelManager
+ /// Interval used for periodic polling account updates.
private let accountUpdateTimerInterval: TimeInterval = 60
private var accountUpdateTimer: DispatchSourceTimer?
- private let deviceData: StoredDeviceData
- private let accountData: StoredAccountData
-
- private let tunnelManager: TunnelManager
+ private let logger = Logger(label: "\(WelcomeInteractor.self)")
private var tunnelObserver: TunnelObserver?
+ private(set) var product: SKProduct?
+
+ var didChangeInAppPurchaseState: ((ProductState) -> Void)?
+ var didAddMoreCredit: (() -> Void)?
+
+ var viewDidLoad = false {
+ didSet {
+ guard viewDidLoad else { return }
+ requestAccessToStore()
+ }
+ }
+
+ var viewWillAppear = false {
+ didSet {
+ guard viewWillAppear else { return }
+ startAccountUpdateTimer()
+ }
+ }
- var didUpdateDeviceState: ((DeviceState) -> Void)?
+ var viewDidDisappear = false {
+ didSet {
+ guard viewDidDisappear else { return }
+ stopAccountUpdateTimer()
+ }
+ }
+
+ var accountNumber: String {
+ tunnelManager.deviceState.accountData?.number ?? ""
+ }
var viewModel: WelcomeViewModel {
WelcomeViewModel(
- deviceName: deviceData.capitalizedName,
- accountNumber: accountData.number.formattedAccountNumber
+ deviceName: tunnelManager.deviceState.deviceData?.capitalizedName ?? "",
+ accountNumber: tunnelManager.deviceState.accountData?.number.formattedAccountNumber ?? ""
)
}
- init(deviceData: StoredDeviceData, accountData: StoredAccountData, tunnelManager: TunnelManager) {
- self.deviceData = deviceData
- self.accountData = accountData
+ init(
+ storePaymentManager: StorePaymentManager,
+ tunnelManager: TunnelManager
+ ) {
+ self.storePaymentManager = storePaymentManager
self.tunnelManager = tunnelManager
-
let tunnelObserver =
- TunnelBlockObserver(didUpdateDeviceState: { [weak self] tunnelManager, deviceState, previousDeviceState in
- self?.didUpdateDeviceState?(deviceState)
+ TunnelBlockObserver(didUpdateDeviceState: { [weak self] _, deviceState, previousDeviceState in
+ let isInactive = previousDeviceState.accountData?.isExpired == true
+ let isActive = deviceState.accountData?.isExpired == false
+ if isInactive && isActive {
+ self?.didAddMoreCredit?()
+ }
})
tunnelManager.addObserver(tunnelObserver)
self.tunnelObserver = tunnelObserver
}
- func startAccountUpdateTimer() {
+ private func requestAccessToStore() {
+ if !StorePaymentManager.canMakePayments {
+ didChangeInAppPurchaseState?(.cannotMakePurchases)
+ } else {
+ let product = StoreSubscription.thirtyDays
+ didChangeInAppPurchaseState?(.fetching(product))
+ _ = storePaymentManager.requestProducts(with: [product]) { [weak self] result in
+ guard let self else { return }
+ let product = result.value?.products.first
+ let productState: ProductState = product.map { .received($0) } ?? .failed
+ didChangeInAppPurchaseState?(productState)
+ self.product = product
+ }
+ }
+ }
+
+ private func startAccountUpdateTimer() {
logger.debug(
"Start polling account updates every \(accountUpdateTimerInterval) second(s)."
)
@@ -61,7 +109,7 @@ class WelcomeInteractor {
timer.activate()
}
- func stopAccountUpdateTimer() {
+ private func stopAccountUpdateTimer() {
logger.debug(
"Stop polling account updates."
)
diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift
index e5594455f8..9de0ce8334 100644
--- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift
+++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift
@@ -6,12 +6,13 @@
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//
+import StoreKit
import UIKit
+
protocol WelcomeViewControllerDelegate: AnyObject {
- func didRequestToPurchaseCredit(controller: WelcomeViewController)
func didRequestToRedeemVoucher(controller: WelcomeViewController)
func didRequestToShowInfo(controller: WelcomeViewController)
- func didUpdateDeviceState(deviceState: DeviceState)
+ func didRequestToPurchaseCredit(controller: WelcomeViewController, accountNumber: String, product: SKProduct)
}
class WelcomeViewController: UIViewController, RootContainment {
@@ -56,27 +57,26 @@ class WelcomeViewController: UIViewController, RootContainment {
override func viewDidLoad() {
super.viewDidLoad()
-
configureUI()
contentView.viewModel = interactor.viewModel
-
- interactor.didUpdateDeviceState = { [weak self] deviceState in
- self?.delegate?.didUpdateDeviceState(deviceState: deviceState)
+ interactor.didChangeInAppPurchaseState = { [weak self] productState in
+ guard let self else { return }
+ self.contentView.productState = productState
}
+ interactor.viewDidLoad = true
}
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- interactor.startAccountUpdateTimer()
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ interactor.viewWillAppear = true
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
- interactor.stopAccountUpdateTimer()
+ interactor.viewDidDisappear = true
}
private func configureUI() {
- view.addSubview(contentView)
view.addConstrainedSubviews([contentView]) {
contentView.pinEdgesToSuperview()
}
@@ -89,10 +89,26 @@ extension WelcomeViewController: WelcomeContentViewDelegate {
}
func didTapPurchaseButton(welcomeContentView: WelcomeContentView, button: AppButton) {
- delegate?.didRequestToPurchaseCredit(controller: self)
+ interactor.product.flatMap {
+ delegate?.didRequestToPurchaseCredit(
+ controller: self,
+ accountNumber: interactor.accountNumber,
+ product: $0
+ )
+ }
}
func didTapRedeemVoucherButton(welcomeContentView: WelcomeContentView, button: AppButton) {
delegate?.didRequestToRedeemVoucher(controller: self)
}
}
+
+extension WelcomeViewController: InAppPurchaseViewControllerDelegate {
+ func didBeginPayment() {
+ contentView.isPurchasing = true
+ }
+
+ func didEndPayment() {
+ contentView.isPurchasing = false
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift b/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift
index cdb09cb7d6..6db0a04b93 100644
--- a/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift
+++ b/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift
@@ -12,6 +12,8 @@ import MullvadLogging
final class LoginInteractor {
private let tunnelManager: TunnelManager
private let logger = Logger(label: "LoginInteractor")
+ private var tunnelObserver: TunnelObserver?
+ var didCreateAccount: (() -> Void)?
init(tunnelManager: TunnelManager) {
self.tunnelManager = tunnelManager
@@ -24,7 +26,8 @@ final class LoginInteractor {
}
func createAccount(completion: @escaping (Result<String, Error>) -> Void) {
- tunnelManager.setNewAccount { result in
+ tunnelManager.setNewAccount { [weak self] result in
+ self?.didCreateAccount?()
completion(result.map { $0.number })
}
}
diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift
index 5c04436359..cd2c4dd15a 100644
--- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift
+++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift
@@ -27,6 +27,7 @@ final class OutOfTimeInteractor {
var didReceivePaymentEvent: ((StorePaymentEvent) -> Void)?
var didReceiveTunnelStatus: ((TunnelStatus) -> Void)?
+ var didAddMoreCredit: (() -> Void)?
init(storePaymentManager: StorePaymentManager, tunnelManager: TunnelManager) {
self.storePaymentManager = storePaymentManager
@@ -35,6 +36,13 @@ final class OutOfTimeInteractor {
let tunnelObserver = TunnelBlockObserver(
didUpdateTunnelStatus: { [weak self] manager, tunnelStatus in
self?.didReceiveTunnelStatus?(tunnelStatus)
+ },
+ didUpdateDeviceState: { [weak self] tunnelManager, deviceState, previousDeviceState in
+ let isInactive = previousDeviceState.accountData?.isExpired == true
+ let isActive = deviceState.accountData?.isExpired == false
+ if isInactive && isActive {
+ self?.didAddMoreCredit?()
+ }
}
)
diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherSucceededViewController.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/AddCreditSucceededViewController.swift
index e8ee6865e9..8ddf300f3e 100644
--- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherSucceededViewController.swift
+++ b/ios/MullvadVPN/View controllers/RedeemVoucher/AddCreditSucceededViewController.swift
@@ -1,5 +1,5 @@
//
-// RedeemVoucherSucceededViewController.swift
+// AddCreditSucceededViewController.swift
// MullvadVPN
//
// Created by Sajad Vishkai on 2022-09-23.
@@ -8,15 +8,15 @@
import UIKit
-protocol RedeemVoucherSucceededViewControllerDelegate: AnyObject {
- func redeemVoucherSucceededViewControllerDidFinish(
- _ controller: RedeemVoucherSucceededViewController
- )
+protocol AddCreditSucceededViewControllerDelegate: AnyObject {
+ func header(in controller: AddCreditSucceededViewController) -> String
- func titleForAction(in controller: RedeemVoucherSucceededViewController) -> String
+ func titleForAction(in controller: AddCreditSucceededViewController) -> String
+
+ func addCreditSucceededViewControllerDidFinish(in controller: AddCreditSucceededViewController)
}
-class RedeemVoucherSucceededViewController: UIViewController {
+class AddCreditSucceededViewController: UIViewController, RootContainment {
private let statusImageView: StatusImageView = {
let statusImageView = StatusImageView(style: .success)
statusImageView.translatesAutoresizingMaskIntoConstraints = false
@@ -26,12 +26,6 @@ class RedeemVoucherSucceededViewController: UIViewController {
private let titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 20)
- label.text = NSLocalizedString(
- "REDEEM_VOUCHER_SUCCESS_TITLE",
- tableName: "RedeemVoucher",
- value: "Voucher was successfully redeemed.",
- comment: ""
- )
label.textColor = .white
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
@@ -57,9 +51,26 @@ class RedeemVoucherSucceededViewController: UIViewController {
.lightContent
}
- weak var delegate: RedeemVoucherSucceededViewControllerDelegate? {
+ var preferredHeaderBarPresentation: HeaderBarPresentation {
+ HeaderBarPresentation(style: .default, showsDivider: true)
+ }
+
+ var prefersHeaderBarHidden: Bool {
+ false
+ }
+
+ var prefersDeviceInfoBarHidden: Bool {
+ true
+ }
+
+ var prefersNotificationBarHidden: Bool {
+ true
+ }
+
+ weak var delegate: AddCreditSucceededViewControllerDelegate? {
didSet {
dismissButton.setTitle(delegate?.titleForAction(in: self), for: .normal)
+ titleLabel.text = delegate?.header(in: self)
}
}
@@ -71,8 +82,8 @@ class RedeemVoucherSucceededViewController: UIViewController {
messageLabel.text = String(
format: NSLocalizedString(
- "REDEEM_VOUCHER_SUCCESS_MESSAGE",
- tableName: "RedeemVoucher",
+ "ADDED_TIME_SUCCESS_MESSAGE",
+ tableName: "AddedTime",
value: "%@ were added to your account.",
comment: ""
),
@@ -127,7 +138,7 @@ class RedeemVoucherSucceededViewController: UIViewController {
}
@objc private func handleDismissTap() {
- delegate?.redeemVoucherSucceededViewControllerDidFinish(self)
+ delegate?.addCreditSucceededViewControllerDidFinish(in: self)
}
}
diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift
index 1036dc5e3c..d5aa957ee6 100644
--- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift
+++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift
@@ -18,7 +18,7 @@ protocol RedeemVoucherViewControllerDelegate: AnyObject {
func redeemVoucherDidCancel(_ controller: RedeemVoucherViewController)
}
-class RedeemVoucherViewController: UIViewController, UINavigationControllerDelegate {
+class RedeemVoucherViewController: UIViewController, UINavigationControllerDelegate, RootContainment {
private let contentView = RedeemVoucherContentView()
private var voucherTask: Cancellable?
private var interactor: RedeemVoucherInteractor?
@@ -38,6 +38,22 @@ class RedeemVoucherViewController: UIViewController, UINavigationControllerDeleg
.lightContent
}
+ var preferredHeaderBarPresentation: HeaderBarPresentation {
+ HeaderBarPresentation(style: .default, showsDivider: true)
+ }
+
+ var prefersHeaderBarHidden: Bool {
+ false
+ }
+
+ var prefersDeviceInfoBarHidden: Bool {
+ true
+ }
+
+ var prefersNotificationBarHidden: Bool {
+ true
+ }
+
// MARK: - Life Cycle
override func viewDidLoad() {