diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index e54ebb4eb0fc..f79339095d9e 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 /* LogoutDialogueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A29782A9F8A9B00EA3B6F /* LogoutDialogueView.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 /* LogoutDialogueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogoutDialogueView.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 /* LogoutDialogueView.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 /* LogoutDialogueView.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..8bb417b1b4e8 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -6,6 +6,7 @@ // Copyright © 2023 Mullvad VPN AB. All rights reserved. // +import Combine import MullvadREST import MullvadTypes import RelayCache @@ -43,6 +44,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo */ private let secondaryNavigationContainer = RootContainerViewController() + /// Posts `preferredAccountNumber` notification when user inputs the account number instead of voucher code + private let preferredAccountNumberSubject = PassthroughSubject() + private lazy var secondaryRootConfiguration = ModalPresentationConfiguration( preferredContentSize: UIMetrics.preferredFormSheetContentSize, modalPresentationStyle: .custom, @@ -70,6 +74,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 +90,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 +98,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 +600,21 @@ 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] _, preferredAccountNumber in + guard let self else { return } + router.dismissAll(.primary, animated: true) + continueFlow(animated: true) + preferredAccountNumberSubject.send(preferredAccountNumber) + } addChild(coordinator) coordinator.start(animated: animated) @@ -634,6 +647,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo devicesProxy: devicesProxy ) + coordinator.preferredAccountNumberPublisher = preferredAccountNumberSubject.eraseToAnyPublisher() + coordinator.didFinish = { [weak self] _ in self?.continueFlow(animated: true) } @@ -682,7 +697,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 68% rename from ios/MullvadVPN/Coordinators/AccountRedeemingVoucherCoordinator.swift rename to ios/MullvadVPN/Coordinators/CreateAccountVoucherCoordinator.swift index 9998fd2d85af..2234af0b0338 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, String) -> Void)? init( navigationController: RootContainerViewController, interactor: RedeemVoucherInteractor ) { self.navigationController = navigationController + self.interactor = interactor viewController = RedeemVoucherViewController(interactor: interactor) } func start() { + interactor.didLogout = { [weak self] accountNumber in + guard let self else { return } + didLogout?(self, accountNumber) + } 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/LoginCoordinator.swift b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift index 444913b059c0..3432240c3f55 100644 --- a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift @@ -6,6 +6,7 @@ // Copyright © 2023 Mullvad VPN AB. All rights reserved. // +import Combine import MullvadREST import MullvadTypes import Operations @@ -18,10 +19,13 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat private var loginController: LoginViewController? private var lastLoginAction: LoginAction? + private var subscriptions = Set() var didFinish: ((LoginCoordinator) -> Void)? var didCreateAccount: (() -> Void)? + var preferredAccountNumberPublisher: AnyPublisher? + let navigationController: RootContainerViewController init( @@ -41,6 +45,14 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat loginController.didFinishLogin = { [weak self] action, error in self?.didFinishLogin(action: action, error: error) ?? .nothing } + + preferredAccountNumberPublisher? + .compactMap { $0 } + .sink(receiveValue: { preferredAccountNumber in + interactor.doSuggestPreferredAccountNumber?(preferredAccountNumber) + }) + .store(in: &subscriptions) + interactor.didCreateAccount = self.didCreateAccount navigationController.pushViewController(loginController, animated: animated) 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..4fd23f1a9fa6 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, String) -> 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] coordinator, accountNumber in + guard let self else { return } + coordinator.removeFromParent() + didLogout?(self, accountNumber) + } + 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/LoginInteractor.swift b/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift index 6db0a04b9352..3590f1a60a40 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift @@ -14,6 +14,7 @@ final class LoginInteractor { private let logger = Logger(label: "LoginInteractor") private var tunnelObserver: TunnelObserver? var didCreateAccount: (() -> Void)? + var doSuggestPreferredAccountNumber: ((String) -> Void)? init(tunnelManager: TunnelManager) { self.tunnelManager = tunnelManager diff --git a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift index 94e51afb557c..bcc0ed418c07 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift @@ -140,6 +140,10 @@ class LoginViewController: UIViewController, RootContainment { self?.attemptLogin() } + interactor.doSuggestPreferredAccountNumber = { [weak self] value in + self?.contentView.accountInputGroup.setAccount(value) + } + contentView.accountInputGroup.setOnReturnKey { [weak self] _ in guard let self else { return true } diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/LogoutDialogueView.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/LogoutDialogueView.swift new file mode 100644 index 000000000000..a6aee49cb53b --- /dev/null +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/LogoutDialogueView.swift @@ -0,0 +1,149 @@ +// +// LogoutDialogueView.swift +// MullvadVPN +// +// Created by Mojgan on 2023-08-29. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class LogoutDialogueView: 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: (LogoutDialogueView) -> Void + + var isLoading = true { + didSet { + logoutButton.isEnabled = !isLoading + } + } + + override var isHidden: Bool { + willSet { + if newValue == true { + fadeOut() + } else { + fadeIn() + } + } + } + + init(didRequestToLogOut: @escaping (LogoutDialogueView) -> 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/RedeemVoucherContentView.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift index a24942bf3a0c..650ced232ba9 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: LogoutDialogueView = { + LogoutDialogueView { 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..fa7639122718 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,71 @@ 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: ((String) -> 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?(accountNumber) + } + } + } + + func cancelAll() { + tasks.forEach { $0.cancel() } + } + + private func verifyVoucherAsAccount(code: String) { + let executer = accountsProxy.getAccountData(accountNumber: code) + tasks.append(executer.execute { [weak self] result in + guard let self, + case .success = result else { + return + } + showLogoutDialog?() + preferredAccountNumber = code + }) + } +} + +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/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) }