From 8a5bcf22b8b1d177da83d1177ad4823b4f8b3b12 Mon Sep 17 00:00:00 2001 From: Mojgan Date: Mon, 11 Sep 2023 14:29:33 +0200 Subject: [PATCH] show dialog when user enters account number instead of voucher code --- ios/MullvadVPN.xcodeproj/project.pbxproj | 44 +++--- .../Coordinators/AccountCoordinator.swift | 16 +- .../Coordinators/ApplicationCoordinator.swift | 15 +- ... => CreateAccountVoucherCoordinator.swift} | 21 +-- ....swift => ProfileVoucherCoordinator.swift} | 17 +- .../Coordinators/WelcomeCoordinator.swift | 22 ++- ios/MullvadVPN/SceneDelegate.swift | 1 + .../Account/AccountInteractor.swift | 8 +- .../Login/LoginContentView.swift | 20 +++ .../RedeemVoucherContentView.swift | 35 +++- .../RedeemVoucherInteractor.swift | 86 +++++++++- .../RedeemVoucherViewController.swift | 29 +++- .../RedeemVoucher/VerifiedAccountView.swift | 149 ++++++++++++++++++ .../RedeemVoucher/VoucherTextField.swift | 4 + .../Settings/SettingsInteractorFactory.swift | 7 - 15 files changed, 403 insertions(+), 71 deletions(-) rename ios/MullvadVPN/Coordinators/{AccountRedeemingVoucherCoordinator.swift => CreateAccountVoucherCoordinator.swift} (70%) rename ios/MullvadVPN/Coordinators/{SettingsRedeemVoucherCoordinator.swift => ProfileVoucherCoordinator.swift} (79%) create mode 100644 ios/MullvadVPN/View controllers/RedeemVoucher/VerifiedAccountView.swift diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index e54ebb4eb0fc..54516d51a8e1 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -422,13 +422,13 @@ 7A9CCCB72A96302800DD6A34 /* RevokedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA52A96302700DD6A34 /* RevokedCoordinator.swift */; }; 7A9CCCB82A96302800DD6A34 /* SetupAccountCompletedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA62A96302700DD6A34 /* SetupAccountCompletedCoordinator.swift */; }; 7A9CCCB92A96302800DD6A34 /* SelectLocationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA72A96302700DD6A34 /* SelectLocationCoordinator.swift */; }; - 7A9CCCBA2A96302800DD6A34 /* AccountRedeemingVoucherCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA82A96302700DD6A34 /* AccountRedeemingVoucherCoordinator.swift */; }; + 7A9CCCBA2A96302800DD6A34 /* CreateAccountVoucherCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA82A96302700DD6A34 /* CreateAccountVoucherCoordinator.swift */; }; 7A9CCCBB2A96302800DD6A34 /* InAppPurchaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA92A96302700DD6A34 /* InAppPurchaseCoordinator.swift */; }; 7A9CCCBC2A96302800DD6A34 /* ChangeLogCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCAA2A96302700DD6A34 /* ChangeLogCoordinator.swift */; }; 7A9CCCBD2A96302800DD6A34 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCAB2A96302800DD6A34 /* LoginCoordinator.swift */; }; 7A9CCCBE2A96302800DD6A34 /* AccountDeletionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCAC2A96302800DD6A34 /* AccountDeletionCoordinator.swift */; }; 7A9CCCBF2A96302800DD6A34 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCAD2A96302800DD6A34 /* SettingsCoordinator.swift */; }; - 7A9CCCC02A96302800DD6A34 /* SettingsRedeemVoucherCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCAE2A96302800DD6A34 /* SettingsRedeemVoucherCoordinator.swift */; }; + 7A9CCCC02A96302800DD6A34 /* ProfileVoucherCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCAE2A96302800DD6A34 /* ProfileVoucherCoordinator.swift */; }; 7A9CCCC12A96302800DD6A34 /* AccountCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCAF2A96302800DD6A34 /* AccountCoordinator.swift */; }; 7A9CCCC22A96302800DD6A34 /* SafariCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCB02A96302800DD6A34 /* SafariCoordinator.swift */; }; 7A9CCCC32A96302800DD6A34 /* ApplicationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCB12A96302800DD6A34 /* ApplicationCoordinator.swift */; }; @@ -483,16 +483,17 @@ E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; }; E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; }; E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */; }; - 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 /* 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 */; }; 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 */; }; F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; }; + F09A297B2A9F8A9B00EA3B6F /* VerifiedAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A29782A9F8A9B00EA3B6F /* VerifiedAccountView.swift */; }; + F09A297C2A9F8A9B00EA3B6F /* VoucherTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A29792A9F8A9B00EA3B6F /* VoucherTextField.swift */; }; + F09A297D2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A297A2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift */; }; + F09A29822A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A297F2A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift */; }; F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; }; F0C6FA812A66E23300F521F0 /* DeleteAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */; }; F0C6FA852A6A733700F521F0 /* InAppPurchaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */; }; @@ -1339,13 +1340,13 @@ 7A9CCCA52A96302700DD6A34 /* RevokedCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokedCoordinator.swift; sourceTree = ""; }; 7A9CCCA62A96302700DD6A34 /* SetupAccountCompletedCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedCoordinator.swift; sourceTree = ""; }; 7A9CCCA72A96302700DD6A34 /* SelectLocationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectLocationCoordinator.swift; sourceTree = ""; }; - 7A9CCCA82A96302700DD6A34 /* AccountRedeemingVoucherCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRedeemingVoucherCoordinator.swift; sourceTree = ""; }; + 7A9CCCA82A96302700DD6A34 /* CreateAccountVoucherCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountVoucherCoordinator.swift; sourceTree = ""; }; 7A9CCCA92A96302700DD6A34 /* InAppPurchaseCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppPurchaseCoordinator.swift; sourceTree = ""; }; 7A9CCCAA2A96302700DD6A34 /* ChangeLogCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeLogCoordinator.swift; sourceTree = ""; }; 7A9CCCAB2A96302800DD6A34 /* LoginCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = ""; }; 7A9CCCAC2A96302800DD6A34 /* AccountDeletionCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountDeletionCoordinator.swift; sourceTree = ""; }; 7A9CCCAD2A96302800DD6A34 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = ""; }; - 7A9CCCAE2A96302800DD6A34 /* SettingsRedeemVoucherCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsRedeemVoucherCoordinator.swift; sourceTree = ""; }; + 7A9CCCAE2A96302800DD6A34 /* ProfileVoucherCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileVoucherCoordinator.swift; sourceTree = ""; }; 7A9CCCAF2A96302800DD6A34 /* AccountCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountCoordinator.swift; sourceTree = ""; }; 7A9CCCB02A96302800DD6A34 /* SafariCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafariCoordinator.swift; sourceTree = ""; }; 7A9CCCB12A96302800DD6A34 /* ApplicationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationCoordinator.swift; sourceTree = ""; }; @@ -1378,17 +1379,18 @@ E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = ""; }; E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = ""; }; E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityView.swift; sourceTree = ""; }; - F028A5482A336E8500C0CAA3 /* VoucherTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoucherTextField.swift; sourceTree = ""; }; - F028A54A2A3370FA00C0CAA3 /* RedeemVoucherContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherContentView.swift; sourceTree = ""; }; F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewController.swift; sourceTree = ""; }; F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddCreditSucceededViewController.swift; sourceTree = ""; }; - F028A56D2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherInteractor.swift; sourceTree = ""; }; F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = ""; }; F0465B5B2A7927B40004089E /* AddCreditSucceededCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCreditSucceededCoordinator.swift; sourceTree = ""; }; F04FBE602A8379EE009278D7 /* AppPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = ""; }; F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = ""; }; F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherOperation.swift; sourceTree = ""; }; F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = ""; }; + F09A29782A9F8A9B00EA3B6F /* VerifiedAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifiedAccountView.swift; sourceTree = ""; }; + F09A29792A9F8A9B00EA3B6F /* VoucherTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoucherTextField.swift; sourceTree = ""; }; + F09A297A2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherContentView.swift; sourceTree = ""; }; + F09A297F2A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherInteractor.swift; sourceTree = ""; }; F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderIdentifier.swift; sourceTree = ""; }; F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountOperation.swift; sourceTree = ""; }; F0C6FA822A6A729500F521F0 /* InAppPurchaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseCoordinator.swift; sourceTree = ""; }; @@ -2307,18 +2309,18 @@ children = ( 7A9CCCAF2A96302800DD6A34 /* AccountCoordinator.swift */, 7A9CCCAC2A96302800DD6A34 /* AccountDeletionCoordinator.swift */, - 7A9CCCA82A96302700DD6A34 /* AccountRedeemingVoucherCoordinator.swift */, 7A9CCCA32A96302700DD6A34 /* AddCreditSucceededCoordinator.swift */, 7A9CCCB12A96302800DD6A34 /* ApplicationCoordinator.swift */, 7A9CCCAA2A96302700DD6A34 /* ChangeLogCoordinator.swift */, + 7A9CCCA82A96302700DD6A34 /* CreateAccountVoucherCoordinator.swift */, 7A9CCCA92A96302700DD6A34 /* InAppPurchaseCoordinator.swift */, 7A9CCCAB2A96302800DD6A34 /* LoginCoordinator.swift */, 7A9CCCA42A96302700DD6A34 /* OutOfTimeCoordinator.swift */, + 7A9CCCAE2A96302800DD6A34 /* ProfileVoucherCoordinator.swift */, 7A9CCCA52A96302700DD6A34 /* RevokedCoordinator.swift */, 7A9CCCB02A96302800DD6A34 /* SafariCoordinator.swift */, 7A9CCCA72A96302700DD6A34 /* SelectLocationCoordinator.swift */, 7A9CCCAD2A96302800DD6A34 /* SettingsCoordinator.swift */, - 7A9CCCAE2A96302800DD6A34 /* SettingsRedeemVoucherCoordinator.swift */, 7A9CCCA62A96302700DD6A34 /* SetupAccountCompletedCoordinator.swift */, 7A9CCCA22A96302700DD6A34 /* TermsOfServiceCoordinator.swift */, 7A9CCCB22A96302800DD6A34 /* TunnelCoordinator.swift */, @@ -2617,11 +2619,12 @@ F028A5472A336E1900C0CAA3 /* RedeemVoucher */ = { isa = PBXGroup; children = ( - F028A54A2A3370FA00C0CAA3 /* RedeemVoucherContentView.swift */, - F028A56D2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift */, F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */, + F09A297A2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift */, + F09A297F2A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift */, F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */, - F028A5482A336E8500C0CAA3 /* VoucherTextField.swift */, + F09A29782A9F8A9B00EA3B6F /* VerifiedAccountView.swift */, + F09A29792A9F8A9B00EA3B6F /* VoucherTextField.swift */, ); path = RedeemVoucher; sourceTree = ""; @@ -3760,6 +3763,7 @@ F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */, 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */, A9F360342AAB626300F53531 /* VPNConnectionProtocol.swift in Sources */, + F09A297D2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift in Sources */, 5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */, 587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */, 586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */, @@ -3781,7 +3785,6 @@ 5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */, F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */, 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */, - F028A56E2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift in Sources */, 5878A27129091CF20096FC88 /* AccountInteractor.swift in Sources */, 068CE5742927B7A400A068BB /* Migration.swift in Sources */, A92ECC282A7802AB0052F1B1 /* StoredDeviceData.swift in Sources */, @@ -3811,7 +3814,6 @@ 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */, F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */, 7A9CCCC12A96302800DD6A34 /* AccountCoordinator.swift in Sources */, - F028A54B2A3370FA00C0CAA3 /* RedeemVoucherContentView.swift in Sources */, 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */, 5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */, F0E3618B2A4ADD2F00AEEF2B /* WelcomeContentView.swift in Sources */, @@ -3819,12 +3821,13 @@ E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */, 7A9CCCBD2A96302800DD6A34 /* LoginCoordinator.swift in Sources */, 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */, + F09A29822A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift in Sources */, 58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */, 5878A279290954790096FC88 /* TunnelViewControllerInteractor.swift in Sources */, 7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */, 7A9CCCBF2A96302800DD6A34 /* SettingsCoordinator.swift in Sources */, 582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */, - 7A9CCCBA2A96302800DD6A34 /* AccountRedeemingVoucherCoordinator.swift in Sources */, + 7A9CCCBA2A96302800DD6A34 /* CreateAccountVoucherCoordinator.swift in Sources */, 5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */, F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedController.swift in Sources */, 5846227726E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift in Sources */, @@ -3921,7 +3924,7 @@ 5878A27D2909657C0096FC88 /* RevokedDeviceInteractor.swift in Sources */, F0E8E4C32A602E0D00ED26A3 /* AccountDeletionViewModel.swift in Sources */, 58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */, - 7A9CCCC02A96302800DD6A34 /* SettingsRedeemVoucherCoordinator.swift in Sources */, + 7A9CCCC02A96302800DD6A34 /* ProfileVoucherCoordinator.swift in Sources */, 7A9CCCBC2A96302800DD6A34 /* ChangeLogCoordinator.swift in Sources */, 58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */, 58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */, @@ -3934,7 +3937,6 @@ 587D9676288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift in Sources */, F028A56C2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift in Sources */, 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */, - F028A5492A336E8500C0CAA3 /* VoucherTextField.swift in Sources */, 58B9EB152489139B00095626 /* RESTError+Display.swift in Sources */, 587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */, 58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */, @@ -3955,9 +3957,11 @@ 7A9CCCB62A96302800DD6A34 /* OutOfTimeCoordinator.swift in Sources */, 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */, 58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */, + F09A297B2A9F8A9B00EA3B6F /* VerifiedAccountView.swift in Sources */, 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */, 7A21DACF2A30AA3700A787A9 /* UITextField+Appearance.swift in Sources */, 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */, + F09A297C2A9F8A9B00EA3B6F /* VoucherTextField.swift in Sources */, 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */, 7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */, 58A8EE5E2976DB00009C0F8D /* StorePaymentManagerError+Display.swift in Sources */, diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift index 516ade62a10c..b1811001c2da 100644 --- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift @@ -77,15 +77,19 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { } private func navigateToRedeemVoucher() { - let coordinator = SettingsRedeemVoucherCoordinator( + let coordinator = ProfileVoucherCoordinator( navigationController: CustomNavigationController(), - interactor: RedeemVoucherInteractor(tunnelManager: interactor.tunnelManager) + interactor: RedeemVoucherInteractor( + tunnelManager: interactor.tunnelManager, + accountsProxy: interactor.accountsProxy, + verifyVoucherAsAccount: false + ) ) - coordinator.didFinish = { redeemVoucherCoordinator in - redeemVoucherCoordinator.dismiss(animated: true) + coordinator.didFinish = { coordinator in + coordinator.dismiss(animated: true) } - coordinator.didCancel = { redeemVoucherCoordinator in - redeemVoucherCoordinator.dismiss(animated: true) + coordinator.didCancel = { coordinator in + coordinator.dismiss(animated: true) } coordinator.start() diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index ae302ea87d36..a881d3d05efa 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -70,6 +70,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private let apiProxy: REST.APIProxy private let devicesProxy: REST.DevicesProxy + private let accountsProxy: REST.AccountsProxy private var tunnelObserver: TunnelObserver? private var appPreferences: AppPreferencesDataSource @@ -85,6 +86,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo relayCacheTracker: RelayCacheTracker, apiProxy: REST.APIProxy, devicesProxy: REST.DevicesProxy, + accountsProxy: REST.AccountsProxy, appPreferences: AppPreferencesDataSource ) { self.tunnelManager = tunnelManager @@ -92,6 +94,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo self.relayCacheTracker = relayCacheTracker self.apiProxy = apiProxy self.devicesProxy = devicesProxy + self.accountsProxy = accountsProxy self.appPreferences = appPreferences super.init() @@ -593,15 +596,20 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo let coordinator = WelcomeCoordinator( navigationController: horizontalFlowController, storePaymentManager: storePaymentManager, - tunnelManager: tunnelManager + tunnelManager: tunnelManager, + accountsProxy: accountsProxy ) - coordinator.didFinish = { [weak self] _ in guard let self else { return } appPreferences.isShownOnboarding = true router.dismiss(.welcome, animated: false) continueFlow(animated: false) } + coordinator.didLogout = { [weak self] _ in + guard let self else { return } + router.dismissAll(.primary, animated: true) + continueFlow(animated: true) + } addChild(coordinator) coordinator.start(animated: animated) @@ -682,7 +690,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private func presentAccount(animated: Bool, completion: @escaping (Coordinator) -> Void) { let accountInteractor = AccountInteractor( storePaymentManager: storePaymentManager, - tunnelManager: tunnelManager + tunnelManager: tunnelManager, + accountsProxy: accountsProxy ) let coordinator = AccountCoordinator( diff --git a/ios/MullvadVPN/Coordinators/AccountRedeemingVoucherCoordinator.swift b/ios/MullvadVPN/Coordinators/CreateAccountVoucherCoordinator.swift similarity index 70% rename from ios/MullvadVPN/Coordinators/AccountRedeemingVoucherCoordinator.swift rename to ios/MullvadVPN/Coordinators/CreateAccountVoucherCoordinator.swift index 9998fd2d85af..d2454d68c507 100644 --- a/ios/MullvadVPN/Coordinators/AccountRedeemingVoucherCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/CreateAccountVoucherCoordinator.swift @@ -1,5 +1,5 @@ // -// AccountRedeemingVoucherCoordinator.swift +// CreateAccountVoucherCoordinator.swift // MullvadVPN // // Created by Mojgan on 2023-07-03. @@ -10,32 +10,35 @@ import MullvadREST import Routing import UIKit -public class AccountRedeemingVoucherCoordinator: Coordinator, Presentable { +public class CreateAccountVoucherCoordinator: Coordinator { private let navigationController: RootContainerViewController private let viewController: RedeemVoucherViewController + private let interactor: RedeemVoucherInteractor - var didFinish: ((AccountRedeemingVoucherCoordinator) -> Void)? - var didCancel: ((AccountRedeemingVoucherCoordinator) -> Void)? - - public var presentedViewController: UIViewController { - viewController - } + var didFinish: ((CreateAccountVoucherCoordinator) -> Void)? + var didCancel: ((CreateAccountVoucherCoordinator) -> Void)? + var didLogout: ((CreateAccountVoucherCoordinator) -> Void)? init( navigationController: RootContainerViewController, interactor: RedeemVoucherInteractor ) { self.navigationController = navigationController + self.interactor = interactor viewController = RedeemVoucherViewController(interactor: interactor) } func start() { + interactor.didLogout = { [weak self] in + guard let self else { return } + didLogout?(self) + } viewController.delegate = self navigationController.pushViewController(viewController, animated: true) } } -extension AccountRedeemingVoucherCoordinator: RedeemVoucherViewControllerDelegate { +extension CreateAccountVoucherCoordinator: RedeemVoucherViewControllerDelegate { func redeemVoucherDidSucceed(_ controller: RedeemVoucherViewController, with response: REST.SubmitVoucherResponse) { let coordinator = AddCreditSucceededCoordinator( purchaseType: .redeemingVoucher, diff --git a/ios/MullvadVPN/Coordinators/SettingsRedeemVoucherCoordinator.swift b/ios/MullvadVPN/Coordinators/ProfileVoucherCoordinator.swift similarity index 79% rename from ios/MullvadVPN/Coordinators/SettingsRedeemVoucherCoordinator.swift rename to ios/MullvadVPN/Coordinators/ProfileVoucherCoordinator.swift index c44d670d633e..02aed1dea22e 100644 --- a/ios/MullvadVPN/Coordinators/SettingsRedeemVoucherCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ProfileVoucherCoordinator.swift @@ -1,5 +1,5 @@ // -// SettingsRedeemVoucherCoordinator.swift +// ProfileVoucherCoordinator.swift // MullvadVPN // // Created by Mojgan on 2023-06-13. @@ -11,11 +11,12 @@ import MullvadREST import Routing import UIKit -final class SettingsRedeemVoucherCoordinator: Coordinator, Presentable { +final class ProfileVoucherCoordinator: Coordinator, Presentable { private let navigationController: UINavigationController private let viewController: RedeemVoucherViewController - var didFinish: ((SettingsRedeemVoucherCoordinator) -> Void)? - var didCancel: ((SettingsRedeemVoucherCoordinator) -> Void)? + + var didFinish: ((ProfileVoucherCoordinator) -> Void)? + var didCancel: ((ProfileVoucherCoordinator) -> Void)? init( navigationController: UINavigationController, @@ -36,7 +37,7 @@ final class SettingsRedeemVoucherCoordinator: Coordinator, Presentable { } } -extension SettingsRedeemVoucherCoordinator: RedeemVoucherViewControllerDelegate { +extension ProfileVoucherCoordinator: RedeemVoucherViewControllerDelegate { func redeemVoucherDidSucceed( _ controller: RedeemVoucherViewController, with response: REST.SubmitVoucherResponse @@ -51,7 +52,7 @@ extension SettingsRedeemVoucherCoordinator: RedeemVoucherViewControllerDelegate } } -extension SettingsRedeemVoucherCoordinator: AddCreditSucceededViewControllerDelegate { +extension ProfileVoucherCoordinator: AddCreditSucceededViewControllerDelegate { func addCreditSucceededViewControllerDidFinish(in controller: AddCreditSucceededViewController) { didFinish?(self) } @@ -59,7 +60,7 @@ extension SettingsRedeemVoucherCoordinator: AddCreditSucceededViewControllerDele func header(in controller: AddCreditSucceededViewController) -> String { NSLocalizedString( "REDEEM_VOUCHER_SUCCESS_TITLE", - tableName: "RedeemVoucher", + tableName: "ProfileRedeemVoucher", value: "Voucher was successfully redeemed.", comment: "" ) @@ -68,7 +69,7 @@ extension SettingsRedeemVoucherCoordinator: AddCreditSucceededViewControllerDele func titleForAction(in controller: AddCreditSucceededViewController) -> String { NSLocalizedString( "REDEEM_VOUCHER_DISMISS_BUTTON", - tableName: "RedeemVoucher", + tableName: "ProfileRedeemVoucher", value: "Got it!", comment: "" ) diff --git a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift index de341fbce850..e0714cdac686 100644 --- a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift @@ -17,10 +17,12 @@ final class WelcomeCoordinator: Coordinator, Presentable, Presenting { private let storePaymentManager: StorePaymentManager private let tunnelManager: TunnelManager private let inAppPurchaseInteractor: InAppPurchaseInteractor + private let accountsProxy: REST.AccountsProxy private var viewController: WelcomeViewController? var didFinish: ((WelcomeCoordinator) -> Void)? + var didLogout: ((WelcomeCoordinator) -> Void)? var presentedViewController: UIViewController { navigationController @@ -33,11 +35,13 @@ final class WelcomeCoordinator: Coordinator, Presentable, Presenting { init( navigationController: RootContainerViewController, storePaymentManager: StorePaymentManager, - tunnelManager: TunnelManager + tunnelManager: TunnelManager, + accountsProxy: REST.AccountsProxy ) { self.navigationController = navigationController self.storePaymentManager = storePaymentManager self.tunnelManager = tunnelManager + self.accountsProxy = accountsProxy self.inAppPurchaseInteractor = InAppPurchaseInteractor(storePaymentManager: storePaymentManager) } @@ -140,9 +144,13 @@ extension WelcomeCoordinator: WelcomeViewControllerDelegate { } func didRequestToRedeemVoucher(controller: WelcomeViewController) { - let coordinator = AccountRedeemingVoucherCoordinator( + let coordinator = CreateAccountVoucherCoordinator( navigationController: navigationController, - interactor: RedeemVoucherInteractor(tunnelManager: tunnelManager) + interactor: RedeemVoucherInteractor( + tunnelManager: tunnelManager, + accountsProxy: accountsProxy, + verifyVoucherAsAccount: true + ) ) coordinator.didCancel = { [weak self] coordinator in @@ -152,11 +160,17 @@ extension WelcomeCoordinator: WelcomeViewControllerDelegate { } coordinator.didFinish = { [weak self] coordinator in - coordinator.removeFromParent() guard let self else { return } + coordinator.removeFromParent() didFinish?(self) } + coordinator.didLogout = { [weak self] voucherCoordinator in + guard let self else { return } + voucherCoordinator.removeFromParent() + didLogout?(self) + } + addChild(coordinator) coordinator.start() diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index c1e9b91f9c4a..b8641b6845dc 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -67,6 +67,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand relayCacheTracker: appDelegate.relayCacheTracker, apiProxy: appDelegate.apiProxy, devicesProxy: appDelegate.devicesProxy, + accountsProxy: appDelegate.accountsProxy, appPreferences: AppPreferences() ) diff --git a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift index 956703bff049..8ad2fb25feaf 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift @@ -15,6 +15,7 @@ import StoreKit final class AccountInteractor { private let storePaymentManager: StorePaymentManager let tunnelManager: TunnelManager + let accountsProxy: REST.AccountsProxy var didReceivePaymentEvent: ((StorePaymentEvent) -> Void)? var didReceiveDeviceState: ((DeviceState) -> Void)? @@ -22,9 +23,14 @@ final class AccountInteractor { private var tunnelObserver: TunnelObserver? private var paymentObserver: StorePaymentObserver? - init(storePaymentManager: StorePaymentManager, tunnelManager: TunnelManager) { + init( + storePaymentManager: StorePaymentManager, + tunnelManager: TunnelManager, + accountsProxy: REST.AccountsProxy + ) { self.storePaymentManager = storePaymentManager self.tunnelManager = tunnelManager + self.accountsProxy = accountsProxy let tunnelObserver = TunnelBlockObserver(didUpdateDeviceState: { [weak self] _, deviceState, _ in diff --git a/ios/MullvadVPN/View controllers/Login/LoginContentView.swift b/ios/MullvadVPN/View controllers/Login/LoginContentView.swift index 5bcce9b6b56f..84338a2c0acf 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginContentView.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginContentView.swift @@ -115,6 +115,7 @@ class LoginContentView: UIView { ) addSubviews() + addObservers() } required init?(coder: NSCoder) { @@ -172,4 +173,23 @@ class LoginContentView: UIView { accountInputGroup.pinEdges(.all().excluding(.bottom), to: accountInputGroupWrapper) } } + + private func addObservers() { + NotificationCenter.default.addObserver( + self, + selector: #selector(didChangePreferredAccountNumber(_:)), + name: RedeemVoucherInteractor.didChangePreferredAccountNumber, + object: nil + ) + } + + @objc private func didChangePreferredAccountNumber(_ notification: Notification) { + guard let preferredAccountNumber = notification + .userInfo?[ + RedeemVoucherInteractor.preferredAccountNumberUserInfoKey + ] as? String else { + return + } + self.accountInputGroup.setAccount(preferredAccountNumber) + } } diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift index a24942bf3a0c..25fcc2a5abca 100644 --- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift @@ -14,6 +14,7 @@ enum RedeemVoucherState { case success case verifying case failure(Error) + case logout } final class RedeemVoucherContentView: UIView { @@ -79,6 +80,13 @@ final class RedeemVoucherContentView: UIView { return label }() + private lazy var logoutViewForAccountNumberIsEntered: VerifiedAccountView = { + VerifiedAccountView { verifiedAccountView in + verifiedAccountView.isLoading = true + self.logoutAction?() + } + }() + private let redeemButton: AppButton = { let button = AppButton(style: .success) button.setTitle(NSLocalizedString( @@ -114,12 +122,14 @@ final class RedeemVoucherContentView: UIView { titleLabel, textField, statusStack, + logoutViewForAccountNumberIsEntered, ]) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.setCustomSpacing(UIMetrics.padding16, after: titleLabel) stackView.setCustomSpacing(UIMetrics.padding8, after: textField) stackView.setCustomSpacing(UIMetrics.padding16, after: statusLabel) + stackView.setCustomSpacing(UIMetrics.padding24, after: statusStack) stackView.setContentHuggingPriority(.defaultLow, for: .vertical) return stackView }() @@ -147,6 +157,13 @@ final class RedeemVoucherContentView: UIView { value: "Verifying voucher...", comment: "" ) + case .logout: + return NSLocalizedString( + "REDEEM_VOUCHER_STATUS_WAITING", + tableName: "RedeemVoucher", + value: "Logging out...", + comment: "" + ) default: return "" } } @@ -155,7 +172,7 @@ final class RedeemVoucherContentView: UIView { switch state { case .initial, .failure: return true - case .success, .verifying: + case .success, .verifying, .logout: return false } } @@ -171,7 +188,7 @@ final class RedeemVoucherContentView: UIView { private var isLoading: Bool { switch state { - case .verifying: + case .verifying, .logout: return true default: return false @@ -185,6 +202,7 @@ final class RedeemVoucherContentView: UIView { var redeemAction: ((String) -> Void)? var cancelAction: (() -> Void)? + var logoutAction: (() -> Void)? var state: RedeemVoucherState = .initial { didSet { @@ -206,6 +224,12 @@ final class RedeemVoucherContentView: UIView { } } + var isLogoutDialogHidden = true { + didSet { + logoutViewForAccountNumberIsEntered.isHidden = isLogoutDialogHidden + } + } + init() { super.init(frame: .zero) commonInit() @@ -276,6 +300,7 @@ final class RedeemVoucherContentView: UIView { redeemButton.isEnabled = isRedeemButtonEnabled && textField.isVoucherLengthSatisfied statusLabel.text = text statusLabel.textColor = textColor + logoutViewForAccountNumberIsEntered.isLoading = isLoading } private func addObservers() { @@ -292,13 +317,17 @@ final class RedeemVoucherContentView: UIView { } @objc private func redeemButtonTapped(_ sender: AppButton) { - guard let code = textField.text, !code.isEmpty else { + let code = textField.parsedToken + guard !code.isEmpty else { return } redeemAction?(code) } @objc private func textDidChange() { + if textField.parsedToken.isEmpty { + isLogoutDialogHidden = true + } updateUI() } diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift index 1fa5a2c6da5a..6f2fe90ef028 100644 --- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift @@ -2,7 +2,7 @@ // RedeemVoucherInteractor.swift // MullvadVPN // -// Created by Mojgan on 2023-05-24. +// Created by Mojgan on 2023-08-30. // Copyright © 2023 Mullvad VPN AB. All rights reserved. // @@ -12,15 +12,93 @@ import MullvadTypes final class RedeemVoucherInteractor { private let tunnelManager: TunnelManager + private let accountsProxy: REST.AccountsProxy + private let shouldVerifyVoucherAsAccount: Bool - init(tunnelManager: TunnelManager) { + private var tasks: [Cancellable] = [] + private var preferredAccountNumber: String? + + var showLogoutDialog: (() -> Void)? + var didLogout: (() -> Void)? + + init( + tunnelManager: TunnelManager, + accountsProxy: REST.AccountsProxy, + verifyVoucherAsAccount: Bool + ) { self.tunnelManager = tunnelManager + self.accountsProxy = accountsProxy + self.shouldVerifyVoucherAsAccount = verifyVoucherAsAccount } func redeemVoucher( code: String, completion: @escaping ((Result) -> Void) - ) -> Cancellable { - tunnelManager.redeemVoucher(code, completion: completion) + ) { + tasks.append(tunnelManager.redeemVoucher(code) { [weak self] result in + guard let self else { return } + completion(result) + guard shouldVerifyVoucherAsAccount, + result.error?.isInvalidVoucher ?? false else { + return + } + verifyVoucherAsAccount(code: code) + }) + } + + func logout(completionHandler: @escaping () -> Void) { + preferredAccountNumber.flatMap { accountNumber in + tunnelManager.unsetAccount { [weak self] in + guard let self else { + return + } + completionHandler() + didLogout?() + notify(accountNumber: accountNumber) + } + } + } + + func cancelAll() { + tasks.forEach { $0.cancel() } + } + + private func verifyVoucherAsAccount(code: String) { + let executer = accountsProxy.getAccountData(accountNumber: code) + tasks.append(executer.execute(retryStrategy: .noRetry) { [weak self] result in + guard let self, + case .success = result else { + return + } + showLogoutDialog?() + preferredAccountNumber = code + }) + } + + /** + Name of notification posted when current account number changes. + */ + static let didChangePreferredAccountNumber = Notification + .Name(rawValue: "CreateAccountVoucherCoordinatorDidChangeAccountNumber") + + /** + User info key passed along with `didChangePreferredAccountNumber` notification that contains string value that + indicates the new account number. + */ + static let preferredAccountNumberUserInfoKey = "preferredAccountNumber" + + /// Posts `didChangePreferredAccountNumber` notification. + private func notify(accountNumber: String) { + NotificationCenter.default.post( + name: Self.didChangePreferredAccountNumber, + object: self, + userInfo: [Self.preferredAccountNumberUserInfoKey: accountNumber] + ) + } +} + +fileprivate extension Error { + var isInvalidVoucher: Bool { + (self as? REST.Error)?.compareErrorCode(.invalidVoucher) ?? false } } diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift index cf96e55ae8a0..f7921f2c6e4b 100644 --- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift @@ -20,14 +20,13 @@ protocol RedeemVoucherViewControllerDelegate: AnyObject { class RedeemVoucherViewController: UIViewController, UINavigationControllerDelegate, RootContainment { private let contentView = RedeemVoucherContentView() - private var voucherTask: Cancellable? - private var interactor: RedeemVoucherInteractor? + private var interactor: RedeemVoucherInteractor weak var delegate: RedeemVoucherViewControllerDelegate? init(interactor: RedeemVoucherInteractor) { - super.init(nibName: nil, bundle: nil) self.interactor = interactor + super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { @@ -87,6 +86,14 @@ class RedeemVoucherViewController: UIViewController, UINavigationControllerDeleg contentView.cancelAction = { [weak self] in self?.cancel() } + + contentView.logoutAction = { [weak self] in + self?.logout() + } + + interactor.showLogoutDialog = { [weak self] in + self?.contentView.isLogoutDialogHidden = false + } } private func configureUI() { @@ -97,12 +104,12 @@ class RedeemVoucherViewController: UIViewController, UINavigationControllerDeleg private func submit(code: String) { contentView.state = .verifying - voucherTask = interactor?.redeemVoucher(code: code, completion: { [weak self] result in + contentView.isEditing = false + interactor.redeemVoucher(code: code, completion: { [weak self] result in guard let self else { return } switch result { case let .success(value): contentView.state = .success - contentView.isEditing = false delegate?.redeemVoucherDidSucceed(self, with: value) case let .failure(error): contentView.state = .failure(error) @@ -113,8 +120,18 @@ class RedeemVoucherViewController: UIViewController, UINavigationControllerDeleg private func cancel() { contentView.isEditing = false - voucherTask?.cancel() + interactor.cancelAll() delegate?.redeemVoucherDidCancel(self) } + + private func logout() { + contentView.isEditing = false + + contentView.state = .logout + + interactor.logout { [weak self] in + self?.contentView.state = .initial + } + } } diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/VerifiedAccountView.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/VerifiedAccountView.swift new file mode 100644 index 000000000000..f6c7fffa4dfd --- /dev/null +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/VerifiedAccountView.swift @@ -0,0 +1,149 @@ +// +// VerifiedAccountView.swift +// MullvadVPN +// +// Created by Mojgan on 2023-08-29. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class VerifiedAccountView: UIView { + private let containerView: UIView = { + let view = UIView() + view.backgroundColor = .secondaryColor + view.layer.cornerRadius = 11 + view.directionalLayoutMargins = UIMetrics.CustomAlert.containerMargins + view.clipsToBounds = true + return view + }() + + private let messageLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .callout, weight: .light) + label.numberOfLines = .zero + label.lineBreakMode = .byWordWrapping + label.textColor = .white + label.text = NSLocalizedString( + "ACCOUNT_NUMBER_AS_VOUCHER_INPUT_ERROR_BODY", + tableName: "CreateAccountRedeemingVoucher", + value: """ + It looks like you have entered a Mullvad account number instead of a voucher code. \ + Do you want to log in to an existing account? + If so, click log out below to log in with the other account number. + """, + comment: "" + ) + return label + }() + + private let logoutButton: AppButton = { + let button = AppButton(style: .danger) + button.setTitle(NSLocalizedString( + "LOGOUT_BUTTON_TITLE", + tableName: "CreateAccountRedeemingVoucher", + value: "Log out", + comment: "" + ), for: .normal) + return button + }() + + private var showConstraint: NSLayoutConstraint? + private var hideConstraint: NSLayoutConstraint? + private var didRequestToLogOut: (VerifiedAccountView) -> Void + + var isLoading = true { + didSet { + logoutButton.isEnabled = !isLoading + } + } + + override var isHidden: Bool { + willSet { + if newValue == true { + fadeOut() + } else { + fadeIn() + } + } + } + + init(didRequestToLogOut: @escaping (VerifiedAccountView) -> Void) { + self.didRequestToLogOut = didRequestToLogOut + super.init(frame: .zero) + setupAppearance() + configureUI() + addActions() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupAppearance() { + containerView.layer.cornerRadius = 11 + containerView.backgroundColor = .primaryColor + } + + private func configureUI() { + addConstrainedSubviews([containerView]) { + containerView.pinEdgesToSuperview(.all().excluding(.bottom)) + } + + containerView.addConstrainedSubviews([messageLabel, logoutButton]) { + messageLabel.pinEdgesToSuperviewMargins(.all().excluding(.bottom)) + logoutButton.pinEdgesToSuperviewMargins(.all().excluding(.top)) + logoutButton.topAnchor.constraint( + equalTo: messageLabel.bottomAnchor, + constant: UIMetrics.padding16 + ).withPriority(.defaultHigh) + } + + showConstraint = containerView.bottomAnchor.constraint(equalTo: bottomAnchor) + hideConstraint = containerView.bottomAnchor.constraint(equalTo: topAnchor) + hideConstraint?.isActive = true + } + + private func addActions() { + logoutButton.addTarget(self, action: #selector(logout), for: .touchUpInside) + } + + @objc private func logout() { + didRequestToLogOut(self) + } + + private func fadeIn() { + guard hideConstraint?.isActive == true else { return } + showConstraint?.isActive = true + hideConstraint?.isActive = false + animateWith(animations: { + self.containerView.alpha = 1.0 + }, duration: 0.3, delay: 0.2) + } + + private func fadeOut() { + guard showConstraint?.isActive == true else { return } + showConstraint?.isActive = false + hideConstraint?.isActive = true + animateWith(animations: { + self.containerView.alpha = 0.0 + }, duration: 0.0, delay: 0.0) + } + + private func animateWith( + animations: @escaping () -> Void, + duration: TimeInterval, + delay: TimeInterval + ) { + UIView.animate( + withDuration: duration, + delay: delay, + options: .curveEaseInOut, + animations: { + animations() + self.layoutIfNeeded() + }, + completion: nil + ) + } +} diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/VoucherTextField.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/VoucherTextField.swift index 89d72228ac71..1475fc939776 100644 --- a/ios/MullvadVPN/View controllers/RedeemVoucher/VoucherTextField.swift +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/VoucherTextField.swift @@ -23,6 +23,10 @@ class VoucherTextField: CustomTextField, UITextFieldDelegate { return maxGroups * groupSize + (maxGroups - 1) } + var parsedToken: String { + inputFormatter.string + } + var isVoucherLengthSatisfied: Bool { let length = text?.count ?? 0 return length >= voucherLength diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift index eee79da6a712..2af17ec14748 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift @@ -28,13 +28,6 @@ final class SettingsInteractorFactory { self.relayCacheTracker = relayCacheTracker } - func makeAccountInteractor() -> AccountInteractor { - AccountInteractor( - storePaymentManager: storePaymentManager, - tunnelManager: tunnelManager - ) - } - func makePreferencesInteractor() -> PreferencesInteractor { PreferencesInteractor(tunnelManager: tunnelManager, relayCacheTracker: relayCacheTracker) }