diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-08-23 13:50:57 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-08-23 13:50:57 +0200 |
| commit | 258a7667e5dab869e7d714fb862ec8f18453907a (patch) | |
| tree | c290ae27695a0ecc920862bdccddad79d7ec6285 | |
| parent | 3413af0cf834f60259fe11d4b45094defd68aa88 (diff) | |
| parent | a02978a7ac0c1e064c5ceb06d778afb739f48ac2 (diff) | |
| download | mullvadvpn-258a7667e5dab869e7d714fb862ec8f18453907a.tar.xz mullvadvpn-258a7667e5dab869e7d714fb862ec8f18453907a.zip | |
Merge branch 'adding-in-app-purchase-in-new-account-flow-ios-228'
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() { |
