diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 811b045d4729..ab6bee364433 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -69,19 +69,15 @@ 582403822A827E1500163DE8 /* RelaySelectorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */; }; 5826B6CB2ABD83E200B1CA13 /* PacketTunnelOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */; }; 5827B0902B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B08F2B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift */; }; - 5827B0922B0CAB2800CCBBA1 /* ProxyConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0912B0CAB2800CCBBA1 /* ProxyConfigurationViewController.swift */; }; - 5827B0942B0CACC700CCBBA1 /* ProxyConfigurationSectionIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0932B0CACC700CCBBA1 /* ProxyConfigurationSectionIdentifier.swift */; }; - 5827B0962B0DB2C100CCBBA1 /* ProxyConfigurationItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0952B0DB2C100CCBBA1 /* ProxyConfigurationItemIdentifier.swift */; }; - 5827B09B2B0DEEF800CCBBA1 /* AccessMethodActionSheetPresentationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B09A2B0DEEF800CCBBA1 /* AccessMethodActionSheetPresentationDelegate.swift */; }; - 5827B09D2B0DEF1000CCBBA1 /* AccessMethodActionSheetDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B09C2B0DEF1000CCBBA1 /* AccessMethodActionSheetDelegate.swift */; }; - 5827B09F2B0E05E600CCBBA1 /* AddAccessMethodInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B09E2B0E05E600CCBBA1 /* AddAccessMethodInteractor.swift */; }; + 5827B0922B0CAB2800CCBBA1 /* MethodSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0912B0CAB2800CCBBA1 /* MethodSettingsViewController.swift */; }; + 5827B0942B0CACC700CCBBA1 /* MethodSettingsSectionIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0932B0CACC700CCBBA1 /* MethodSettingsSectionIdentifier.swift */; }; + 5827B0962B0DB2C100CCBBA1 /* MethodSettingsItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0952B0DB2C100CCBBA1 /* MethodSettingsItemIdentifier.swift */; }; 5827B0A12B0E064E00CCBBA1 /* AccessMethodRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0A02B0E064E00CCBBA1 /* AccessMethodRepository.swift */; }; 5827B0A42B0F38FD00CCBBA1 /* EditAccessMethodInteractorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0A32B0F38FD00CCBBA1 /* EditAccessMethodInteractorProtocol.swift */; }; 5827B0A62B0F39E900CCBBA1 /* EditAccessMethodInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0A52B0F39E900CCBBA1 /* EditAccessMethodInteractor.swift */; }; 5827B0A82B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0A72B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift */; }; 5827B0AA2B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0A92B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift */; }; - 5827B0AC2B0F4CA500CCBBA1 /* AddAccessMethodViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0AB2B0F4CA500CCBBA1 /* AddAccessMethodViewControllerDelegate.swift */; }; - 5827B0AE2B0F4CBE00CCBBA1 /* ProxyConfigurationViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0AD2B0F4CBE00CCBBA1 /* ProxyConfigurationViewControllerDelegate.swift */; }; + 5827B0AE2B0F4CBE00CCBBA1 /* MethodSettingsViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0AD2B0F4CBE00CCBBA1 /* MethodSettingsViewControllerDelegate.swift */; }; 5827B0B02B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0AF2B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift */; }; 5827B0B92B14A1C700CCBBA1 /* MethodTestingStatusCellContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0B82B14A1C700CCBBA1 /* MethodTestingStatusCellContentConfiguration.swift */; }; 5827B0BB2B14A28300CCBBA1 /* MethodTestingStatusCellContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0BA2B14A28300CCBBA1 /* MethodTestingStatusCellContentView.swift */; }; @@ -150,7 +146,6 @@ 586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB122488ED2100095626 /* AlertPresenter.swift */; }; 586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; }; 586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114028F841390037AF9A /* AddressCacheTracker.swift */; }; - 586C0D762B03934000E7CDD7 /* AddAccessMethodInteractorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D752B03934000E7CDD7 /* AddAccessMethodInteractorProtocol.swift */; }; 586C0D782B039CC000E7CDD7 /* AccessMethodProtocolPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D772B039CC000E7CDD7 /* AccessMethodProtocolPicker.swift */; }; 586C0D7A2B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D792B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift */; }; 586C0D7C2B03BDD100E7CDD7 /* AccessMethodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D7B2B03BDD100E7CDD7 /* AccessMethodViewModel.swift */; }; @@ -159,17 +154,12 @@ 586C0D852B03D31E00E7CDD7 /* SocksSectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D842B03D31E00E7CDD7 /* SocksSectionHandler.swift */; }; 586C0D872B03D39600E7CDD7 /* AccessMethodCellReuseIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D862B03D39600E7CDD7 /* AccessMethodCellReuseIdentifier.swift */; }; 586C0D892B03D5E000E7CDD7 /* TextCellContentConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D882B03D5E000E7CDD7 /* TextCellContentConfiguration+Extensions.swift */; }; - 586C0D8B2B03D84400E7CDD7 /* AddAccessMethodItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D8A2B03D84400E7CDD7 /* AddAccessMethodItemIdentifier.swift */; }; - 586C0D8D2B03D86F00E7CDD7 /* AddAccessMethodSectionIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D8C2B03D86F00E7CDD7 /* AddAccessMethodSectionIdentifier.swift */; }; 586C0D8F2B03D88100E7CDD7 /* ProxyProtocolConfigurationItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D8E2B03D88100E7CDD7 /* ProxyProtocolConfigurationItemIdentifier.swift */; }; 586C0D912B03D8A400E7CDD7 /* AccessMethodHeaderFooterReuseIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D902B03D8A400E7CDD7 /* AccessMethodHeaderFooterReuseIdentifier.swift */; }; 586C0D932B03D90700E7CDD7 /* ShadowsocksItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D922B03D90700E7CDD7 /* ShadowsocksItemIdentifier.swift */; }; 586C0D952B03D92100E7CDD7 /* SocksItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D942B03D92100E7CDD7 /* SocksItemIdentifier.swift */; }; 586C0D972B04E0AC00E7CDD7 /* PersistentAccessMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */; }; 586C0D992B04E20200E7CDD7 /* AccessMethodViewModel+Persistent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D982B04E20200E7CDD7 /* AccessMethodViewModel+Persistent.swift */; }; - 586C0D9B2B051E6900E7CDD7 /* AccessMethodActionSheetContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D9A2B051E6900E7CDD7 /* AccessMethodActionSheetContainerView.swift */; }; - 586C0D9D2B05F62B00E7CDD7 /* AccessMethodActionSheetContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D9C2B05F62B00E7CDD7 /* AccessMethodActionSheetContentView.swift */; }; - 586C0DA22B062B2900E7CDD7 /* AccessMethodActionSheetPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0DA12B062B2900E7CDD7 /* AccessMethodActionSheetPresentation.swift */; }; 586C14582AC463BB00245C01 /* CommandChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14572AC463BB00245C01 /* CommandChannelTests.swift */; }; 586C145A2AC4735F00245C01 /* PacketTunnelActor+Public.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */; }; 586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */; }; @@ -419,17 +409,12 @@ 58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */; }; 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */; }; 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF581025D69DB400AEBA94 /* StatusImageView.swift */; }; - 58EF874F2B16174A00C098B2 /* AccessMethodActionSheetPresentationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF874E2B16174A00C098B2 /* AccessMethodActionSheetPresentationConfiguration.swift */; }; - 58EF87512B16176300C098B2 /* AccessMethodActionSheetContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF87502B16176300C098B2 /* AccessMethodActionSheetContentConfiguration.swift */; }; - 58EF87532B161D7C00C098B2 /* AccessMethodActionSheetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF87522B161D7C00C098B2 /* AccessMethodActionSheetConfiguration.swift */; }; - 58EF87552B16282D00C098B2 /* AccessMethodActionSheetPresentationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF87542B16282D00C098B2 /* AccessMethodActionSheetPresentationView.swift */; }; 58EF87572B16330B00C098B2 /* ProxyConfigurationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF87562B16330B00C098B2 /* ProxyConfigurationTester.swift */; }; 58EF875B2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF875A2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift */; }; 58EF875D2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF875C2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift */; }; 58EFC76A2AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EFC7692AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift */; }; 58EFC76E2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EFC76D2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift */; }; 58EFC7712AFB45E500E9F4CB /* SettingsChildCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EFC7702AFB45E500E9F4CB /* SettingsChildCoordinator.swift */; }; - 58EFC7732AFB471500E9F4CB /* AddAccessMethodViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EFC7722AFB471500E9F4CB /* AddAccessMethodViewController.swift */; }; 58EFC7752AFB4CEF00E9F4CB /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EFC7742AFB4CEF00E9F4CB /* AboutViewController.swift */; }; 58F0974E2A20C31100DA2DAD /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58F0974D2A20C31100DA2DAD /* WireGuardKitTypes */; }; 58F0974F2A20C31100DA2DAD /* WireGuardKitTypes in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 58F0974D2A20C31100DA2DAD /* WireGuardKitTypes */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -509,6 +494,7 @@ 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; }; 7A5869952B32E9C700640D27 /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869942B32E9C700640D27 /* LinkButton.swift */; }; 7A5869972B32EA4500640D27 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869962B32EA4500640D27 /* AppButton.swift */; }; + 7A58699B2B482FE200640D27 /* UITableViewCell+Disable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */; }; 7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; }; 7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; }; 7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; }; @@ -1262,19 +1248,15 @@ 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorProtocol.swift; sourceTree = ""; }; 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapper.swift; sourceTree = ""; }; 5827B08F2B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessMethodCoordinator.swift; sourceTree = ""; }; - 5827B0912B0CAB2800CCBBA1 /* ProxyConfigurationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyConfigurationViewController.swift; sourceTree = ""; }; - 5827B0932B0CACC700CCBBA1 /* ProxyConfigurationSectionIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ProxyConfigurationSectionIdentifier.swift; path = ../ProxyConfigurationSectionIdentifier.swift; sourceTree = ""; }; - 5827B0952B0DB2C100CCBBA1 /* ProxyConfigurationItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyConfigurationItemIdentifier.swift; sourceTree = ""; }; - 5827B09A2B0DEEF800CCBBA1 /* AccessMethodActionSheetPresentationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodActionSheetPresentationDelegate.swift; sourceTree = ""; }; - 5827B09C2B0DEF1000CCBBA1 /* AccessMethodActionSheetDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodActionSheetDelegate.swift; sourceTree = ""; }; - 5827B09E2B0E05E600CCBBA1 /* AddAccessMethodInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessMethodInteractor.swift; sourceTree = ""; }; + 5827B0912B0CAB2800CCBBA1 /* MethodSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsViewController.swift; sourceTree = ""; }; + 5827B0932B0CACC700CCBBA1 /* MethodSettingsSectionIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MethodSettingsSectionIdentifier.swift; path = ../MethodSettingsSectionIdentifier.swift; sourceTree = ""; }; + 5827B0952B0DB2C100CCBBA1 /* MethodSettingsItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsItemIdentifier.swift; sourceTree = ""; }; 5827B0A02B0E064E00CCBBA1 /* AccessMethodRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodRepository.swift; sourceTree = ""; }; 5827B0A32B0F38FD00CCBBA1 /* EditAccessMethodInteractorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessMethodInteractorProtocol.swift; sourceTree = ""; }; 5827B0A52B0F39E900CCBBA1 /* EditAccessMethodInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessMethodInteractor.swift; sourceTree = ""; }; 5827B0A72B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyConfigurationInteractorProtocol.swift; sourceTree = ""; }; 5827B0A92B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessMethodViewControllerDelegate.swift; sourceTree = ""; }; - 5827B0AB2B0F4CA500CCBBA1 /* AddAccessMethodViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessMethodViewControllerDelegate.swift; sourceTree = ""; }; - 5827B0AD2B0F4CBE00CCBBA1 /* ProxyConfigurationViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyConfigurationViewControllerDelegate.swift; sourceTree = ""; }; + 5827B0AD2B0F4CBE00CCBBA1 /* MethodSettingsViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsViewControllerDelegate.swift; sourceTree = ""; }; 5827B0AF2B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessMethodViewControllerDelegate.swift; sourceTree = ""; }; 5827B0B82B14A1C700CCBBA1 /* MethodTestingStatusCellContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodTestingStatusCellContentConfiguration.swift; sourceTree = ""; }; 5827B0BA2B14A28300CCBBA1 /* MethodTestingStatusCellContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodTestingStatusCellContentView.swift; sourceTree = ""; }; @@ -1357,7 +1339,6 @@ 58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationTests.swift; sourceTree = ""; }; 586A95112901321B007BAF2B /* IPv6Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv6Endpoint.swift; sourceTree = ""; }; 586A951329013235007BAF2B /* AnyIPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPEndpoint.swift; sourceTree = ""; }; - 586C0D752B03934000E7CDD7 /* AddAccessMethodInteractorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessMethodInteractorProtocol.swift; sourceTree = ""; }; 586C0D772B039CC000E7CDD7 /* AccessMethodProtocolPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodProtocolPicker.swift; sourceTree = ""; }; 586C0D792B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksCipherPicker.swift; sourceTree = ""; }; 586C0D7B2B03BDD100E7CDD7 /* AccessMethodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodViewModel.swift; sourceTree = ""; }; @@ -1366,17 +1347,12 @@ 586C0D842B03D31E00E7CDD7 /* SocksSectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocksSectionHandler.swift; sourceTree = ""; }; 586C0D862B03D39600E7CDD7 /* AccessMethodCellReuseIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodCellReuseIdentifier.swift; sourceTree = ""; }; 586C0D882B03D5E000E7CDD7 /* TextCellContentConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextCellContentConfiguration+Extensions.swift"; sourceTree = ""; }; - 586C0D8A2B03D84400E7CDD7 /* AddAccessMethodItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessMethodItemIdentifier.swift; sourceTree = ""; }; - 586C0D8C2B03D86F00E7CDD7 /* AddAccessMethodSectionIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessMethodSectionIdentifier.swift; sourceTree = ""; }; 586C0D8E2B03D88100E7CDD7 /* ProxyProtocolConfigurationItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyProtocolConfigurationItemIdentifier.swift; sourceTree = ""; }; 586C0D902B03D8A400E7CDD7 /* AccessMethodHeaderFooterReuseIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodHeaderFooterReuseIdentifier.swift; sourceTree = ""; }; 586C0D922B03D90700E7CDD7 /* ShadowsocksItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksItemIdentifier.swift; sourceTree = ""; }; 586C0D942B03D92100E7CDD7 /* SocksItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocksItemIdentifier.swift; sourceTree = ""; }; 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentAccessMethod.swift; sourceTree = ""; }; 586C0D982B04E20200E7CDD7 /* AccessMethodViewModel+Persistent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessMethodViewModel+Persistent.swift"; sourceTree = ""; }; - 586C0D9A2B051E6900E7CDD7 /* AccessMethodActionSheetContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodActionSheetContainerView.swift; sourceTree = ""; }; - 586C0D9C2B05F62B00E7CDD7 /* AccessMethodActionSheetContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodActionSheetContentView.swift; sourceTree = ""; }; - 586C0DA12B062B2900E7CDD7 /* AccessMethodActionSheetPresentation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessMethodActionSheetPresentation.swift; sourceTree = ""; }; 586C14572AC463BB00245C01 /* CommandChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandChannelTests.swift; sourceTree = ""; }; 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+Public.swift"; sourceTree = ""; }; 586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTunnelProviderMessageOperation.swift; sourceTree = ""; }; @@ -1575,17 +1551,12 @@ 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsDataSourceDelegate.swift; sourceTree = ""; }; 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportSubmissionOverlayView.swift; sourceTree = ""; }; 58EF581025D69DB400AEBA94 /* StatusImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusImageView.swift; sourceTree = ""; }; - 58EF874E2B16174A00C098B2 /* AccessMethodActionSheetPresentationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodActionSheetPresentationConfiguration.swift; sourceTree = ""; }; - 58EF87502B16176300C098B2 /* AccessMethodActionSheetContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodActionSheetContentConfiguration.swift; sourceTree = ""; }; - 58EF87522B161D7C00C098B2 /* AccessMethodActionSheetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodActionSheetConfiguration.swift; sourceTree = ""; }; - 58EF87542B16282D00C098B2 /* AccessMethodActionSheetPresentationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodActionSheetPresentationView.swift; sourceTree = ""; }; 58EF87562B16330B00C098B2 /* ProxyConfigurationTester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyConfigurationTester.swift; sourceTree = ""; }; 58EF875A2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodRepositoryProtocol.swift; sourceTree = ""; }; 58EF875C2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyConfigurationTesterProtocol.swift; sourceTree = ""; }; 58EFC7692AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessMethodHeaderView.swift; sourceTree = ""; }; 58EFC76D2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessMethodCoordinator.swift; sourceTree = ""; }; 58EFC7702AFB45E500E9F4CB /* SettingsChildCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsChildCoordinator.swift; sourceTree = ""; }; - 58EFC7722AFB471500E9F4CB /* AddAccessMethodViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessMethodViewController.swift; sourceTree = ""; }; 58EFC7742AFB4CEF00E9F4CB /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = ""; }; 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerActivityIndicatorView.swift; sourceTree = ""; }; @@ -1650,6 +1621,7 @@ 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = ""; }; 7A5869942B32E9C700640D27 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = ""; }; 7A5869962B32EA4500640D27 /* AppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = ""; }; + 7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Disable.swift"; sourceTree = ""; }; 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = ""; }; 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = ""; }; 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = ""; }; @@ -2050,25 +2022,9 @@ path = MullvadTypes; sourceTree = ""; }; - 581DFAEF2B187606005D6D1C /* Presentation */ = { - isa = PBXGroup; - children = ( - 586C0DA12B062B2900E7CDD7 /* AccessMethodActionSheetPresentation.swift */, - 58EF874E2B16174A00C098B2 /* AccessMethodActionSheetPresentationConfiguration.swift */, - 5827B09A2B0DEEF800CCBBA1 /* AccessMethodActionSheetPresentationDelegate.swift */, - 58EF87542B16282D00C098B2 /* AccessMethodActionSheetPresentationView.swift */, - ); - path = Presentation; - sourceTree = ""; - }; 581DFAF02B187620005D6D1C /* Content view */ = { isa = PBXGroup; children = ( - 58EF87522B161D7C00C098B2 /* AccessMethodActionSheetConfiguration.swift */, - 586C0D9A2B051E6900E7CDD7 /* AccessMethodActionSheetContainerView.swift */, - 58EF87502B16176300C098B2 /* AccessMethodActionSheetContentConfiguration.swift */, - 586C0D9C2B05F62B00E7CDD7 /* AccessMethodActionSheetContentView.swift */, - 5827B09C2B0DEF1000CCBBA1 /* AccessMethodActionSheetDelegate.swift */, ); path = "Content view"; sourceTree = ""; @@ -2128,15 +2084,17 @@ path = Common; sourceTree = ""; }; - 5827B0992B0DC0CA00CCBBA1 /* ProxyConfiguration */ = { + 5827B0992B0DC0CA00CCBBA1 /* MethodSettings */ = { isa = PBXGroup; children = ( - 5827B0952B0DB2C100CCBBA1 /* ProxyConfigurationItemIdentifier.swift */, - 5827B0932B0CACC700CCBBA1 /* ProxyConfigurationSectionIdentifier.swift */, - 5827B0912B0CAB2800CCBBA1 /* ProxyConfigurationViewController.swift */, - 5827B0AD2B0F4CBE00CCBBA1 /* ProxyConfigurationViewControllerDelegate.swift */, + 5827B0952B0DB2C100CCBBA1 /* MethodSettingsItemIdentifier.swift */, + 5827B0932B0CACC700CCBBA1 /* MethodSettingsSectionIdentifier.swift */, + 5827B0912B0CAB2800CCBBA1 /* MethodSettingsViewController.swift */, + 5827B0AD2B0F4CBE00CCBBA1 /* MethodSettingsViewControllerDelegate.swift */, + 5827B0B82B14A1C700CCBBA1 /* MethodTestingStatusCellContentConfiguration.swift */, + 5827B0BA2B14A28300CCBBA1 /* MethodTestingStatusCellContentView.swift */, ); - path = ProxyConfiguration; + path = MethodSettings; sourceTree = ""; }; 5827B0A22B0E068800CCBBA1 /* AccessMethodRepository */ = { @@ -2371,6 +2329,7 @@ 7A0C0F622A979C4A0058EFCE /* Coordinator+Router.swift */, 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */, 58DFF7CF2B02560400F864E0 /* NSAttributedString+Markdown.swift */, + 5827B0C42B14D3E800CCBBA1 /* NSDiffableDataSourceSnapshot+Reconfigure.swift */, 587D9675288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift */, 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */, 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */, @@ -2387,9 +2346,9 @@ 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */, 58CEB2FA2AFD13E600E6E088 /* UIListContentConfiguration+Extensions.swift */, 58CEB2FC2AFD19D300E6E088 /* UITableView+ReuseIdentifier.swift */, + 7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */, 7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */, 5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */, - 5827B0C42B14D3E800CCBBA1 /* NSDiffableDataSourceSnapshot+Reconfigure.swift */, ); path = Extensions; sourceTree = ""; @@ -2593,8 +2552,6 @@ 58FF9FE92B07653800E4C97D /* ButtonCellContentView.swift */, 58CEB3092AFD584700E6E088 /* CustomCellDisclosureHandling.swift */, 58CEB30B2AFD586600E6E088 /* DynamicBackgroundConfiguration.swift */, - 5827B0B82B14A1C700CCBBA1 /* MethodTestingStatusCellContentConfiguration.swift */, - 5827B0BA2B14A28300CCBBA1 /* MethodTestingStatusCellContentView.swift */, 58CEB3012AFD365600E6E088 /* SwitchCellContentConfiguration.swift */, 58CEB3032AFD36CE00E6E088 /* SwitchCellContentView.swift */, 58CEB2F42AFD0BB500E6E088 /* TextCellContentConfiguration.swift */, @@ -2608,7 +2565,6 @@ isa = PBXGroup; children = ( 581DFAF02B187620005D6D1C /* Content view */, - 581DFAEF2B187606005D6D1C /* Presentation */, ); path = Sheet; sourceTree = ""; @@ -3012,12 +2968,6 @@ isa = PBXGroup; children = ( 58CEB2E82AFBBA4A00E6E088 /* AddAccessMethodCoordinator.swift */, - 5827B09E2B0E05E600CCBBA1 /* AddAccessMethodInteractor.swift */, - 586C0D752B03934000E7CDD7 /* AddAccessMethodInteractorProtocol.swift */, - 586C0D8A2B03D84400E7CDD7 /* AddAccessMethodItemIdentifier.swift */, - 586C0D8C2B03D86F00E7CDD7 /* AddAccessMethodSectionIdentifier.swift */, - 58EFC7722AFB471500E9F4CB /* AddAccessMethodViewController.swift */, - 5827B0AB2B0F4CA500CCBBA1 /* AddAccessMethodViewControllerDelegate.swift */, ); path = Add; sourceTree = ""; @@ -3202,7 +3152,7 @@ 58FF9FE12B075BA600E4C97D /* EditAccessMethodSectionIdentifier.swift */, 58FF9FDF2B075ABC00E4C97D /* EditAccessMethodViewController.swift */, 5827B0A92B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift */, - 5827B0992B0DC0CA00CCBBA1 /* ProxyConfiguration */, + 5827B0992B0DC0CA00CCBBA1 /* MethodSettings */, 5827B0A72B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift */, ); path = Edit; @@ -4606,7 +4556,6 @@ 587B75412668FD7800DEF7E9 /* AccountExpirySystemNotificationProvider.swift in Sources */, 587988C728A2A01F00E3DF54 /* AccountDataThrottling.swift in Sources */, F04FBE612A8379EE009278D7 /* AppPreferences.swift in Sources */, - 586C0DA22B062B2900E7CDD7 /* AccessMethodActionSheetPresentation.swift in Sources */, 58DFF7D82B02774C00F864E0 /* ListItemPickerViewController.swift in Sources */, 5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */, 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */, @@ -4615,6 +4564,7 @@ 7AC8A3AF2ABC71D600DC4939 /* TermsOfServiceCoordinator.swift in Sources */, 58FF9FE22B075BA600E4C97D /* EditAccessMethodSectionIdentifier.swift in Sources */, F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */, + 7A58699B2B482FE200640D27 /* UITableViewCell+Disable.swift in Sources */, 7A9CCCB72A96302800DD6A34 /* RevokedCoordinator.swift in Sources */, 587D96742886D87C00CD8F1C /* DeviceManagementContentView.swift in Sources */, 7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */, @@ -4635,7 +4585,7 @@ 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */, 7A9CCCB32A96302800DD6A34 /* WelcomeCoordinator.swift in Sources */, 587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */, - 5827B0922B0CAB2800CCBBA1 /* ProxyConfigurationViewController.swift in Sources */, + 5827B0922B0CAB2800CCBBA1 /* MethodSettingsViewController.swift in Sources */, 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */, 5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */, 7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */, @@ -4667,11 +4617,10 @@ 588D7ED62AF3903F005DF40A /* ListAccessMethodViewController.swift in Sources */, 582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */, 7A9FA1442A2E3FE5000B728D /* CheckableSettingsCell.swift in Sources */, - 586C0D8D2B03D86F00E7CDD7 /* AddAccessMethodSectionIdentifier.swift in Sources */, 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */, 7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */, 58C774BE29A7A249003A1A56 /* CustomNavigationController.swift in Sources */, - 5827B0942B0CACC700CCBBA1 /* ProxyConfigurationSectionIdentifier.swift in Sources */, + 5827B0942B0CACC700CCBBA1 /* MethodSettingsSectionIdentifier.swift in Sources */, E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */, 7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */, 0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */, @@ -4689,11 +4638,9 @@ 586C0D972B04E0AC00E7CDD7 /* PersistentAccessMethod.swift in Sources */, 58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */, 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */, - 5827B09D2B0DEF1000CCBBA1 /* AccessMethodActionSheetDelegate.swift in Sources */, F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */, 7A9CCCC12A96302800DD6A34 /* AccountCoordinator.swift in Sources */, 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */, - 58EF87532B161D7C00C098B2 /* AccessMethodActionSheetConfiguration.swift in Sources */, 5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */, F0E3618B2A4ADD2F00AEEF2B /* WelcomeContentView.swift in Sources */, 58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */, @@ -4724,9 +4671,7 @@ 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */, 5864859929A0D028006C5743 /* FormsheetPresentationController.swift in Sources */, 58CEB3022AFD365600E6E088 /* SwitchCellContentConfiguration.swift in Sources */, - 586C0D9D2B05F62B00E7CDD7 /* AccessMethodActionSheetContentView.swift in Sources */, 7A9CCCB52A96302800DD6A34 /* AddCreditSucceededCoordinator.swift in Sources */, - 58EF87552B16282D00C098B2 /* AccessMethodActionSheetPresentationView.swift in Sources */, 7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */, 7A6F2FAB2AFD3097006D0856 /* CustomDNSCellFactory.swift in Sources */, 58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */, @@ -4749,14 +4694,12 @@ 5878A27729093A4F0096FC88 /* StorePaymentBlockObserver.swift in Sources */, 58EF87572B16330B00C098B2 /* ProxyConfigurationTester.swift in Sources */, 5827B0A62B0F39E900CCBBA1 /* EditAccessMethodInteractor.swift in Sources */, - 5827B0AE2B0F4CBE00CCBBA1 /* ProxyConfigurationViewControllerDelegate.swift in Sources */, - 5827B0962B0DB2C100CCBBA1 /* ProxyConfigurationItemIdentifier.swift in Sources */, - 586C0D8B2B03D84400E7CDD7 /* AddAccessMethodItemIdentifier.swift in Sources */, + 5827B0AE2B0F4CBE00CCBBA1 /* MethodSettingsViewControllerDelegate.swift in Sources */, + 5827B0962B0DB2C100CCBBA1 /* MethodSettingsItemIdentifier.swift in Sources */, 5868585524054096000B8131 /* CustomButton.swift in Sources */, 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */, 7A1A26492A29D48A00B978AA /* RelayFilterCellFactory.swift in Sources */, 5867771629097C5B006F721F /* ProductState.swift in Sources */, - 586C0D762B03934000E7CDD7 /* AddAccessMethodInteractorProtocol.swift in Sources */, 58C76A082A33850E00100D75 /* ApplicationTarget.swift in Sources */, 58CEB3042AFD36CE00E6E088 /* SwitchCellContentView.swift in Sources */, F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */, @@ -4776,7 +4719,6 @@ 58CEB2F32AFD0BA100E6E088 /* TextCellContentView.swift in Sources */, 586C0D7C2B03BDD100E7CDD7 /* AccessMethodViewModel.swift in Sources */, 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */, - 58EF874F2B16174A00C098B2 /* AccessMethodActionSheetPresentationConfiguration.swift in Sources */, 7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */, 5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */, 7A1A26452A29CEF700B978AA /* RelayFilterViewController.swift in Sources */, @@ -4800,7 +4742,6 @@ 5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */, 583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */, 7A6F2FA92AFD0842006D0856 /* CustomDNSDataSource.swift in Sources */, - 5827B0AC2B0F4CA500CCBBA1 /* AddAccessMethodViewControllerDelegate.swift in Sources */, 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */, 580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */, @@ -4816,9 +4757,7 @@ 5827B0A82B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift in Sources */, 586C0D7A2B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift in Sources */, 58EFC76A2AFAC3B800E9F4CB /* ListAccessMethodHeaderView.swift in Sources */, - 58EFC7732AFB471500E9F4CB /* AddAccessMethodViewController.swift in Sources */, 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */, - 5827B09F2B0E05E600CCBBA1 /* AddAccessMethodInteractor.swift in Sources */, 58FF9FEC2B07A7CB00E4C97D /* NSDirectionalEdgeInsets+Helpers.swift in Sources */, 586C0D832B03D2FF00E7CDD7 /* ShadowsocksSectionHandler.swift in Sources */, 58B26E262943522400D5980C /* NotificationProvider.swift in Sources */, @@ -4836,13 +4775,11 @@ 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */, 58FF9FEA2B07653800E4C97D /* ButtonCellContentView.swift in Sources */, F0E8E4C12A602CCB00ED26A3 /* AccountDeletionContentView.swift in Sources */, - 58EF87512B16176300C098B2 /* AccessMethodActionSheetContentConfiguration.swift in Sources */, 58B26E1E2943514300D5980C /* InAppNotificationDescriptor.swift in Sources */, 58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */, 586C0D992B04E20200E7CDD7 /* AccessMethodViewModel+Persistent.swift in Sources */, 58EF875B2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift in Sources */, 5878A27D2909657C0096FC88 /* RevokedDeviceInteractor.swift in Sources */, - 5827B09B2B0DEEF800CCBBA1 /* AccessMethodActionSheetPresentationDelegate.swift in Sources */, F0E8E4C32A602E0D00ED26A3 /* AccountDeletionViewModel.swift in Sources */, 586C0D782B039CC000E7CDD7 /* AccessMethodProtocolPicker.swift in Sources */, 58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */, @@ -4913,7 +4850,6 @@ 5827B0C52B14D3E800CCBBA1 /* NSDiffableDataSourceSnapshot+Reconfigure.swift in Sources */, 58A8EE5E2976DB00009C0F8D /* StorePaymentManagerError+Display.swift in Sources */, 58A8EE5A2976BFBB009C0F8D /* SKError+Localized.swift in Sources */, - 586C0D9B2B051E6900E7CDD7 /* AccessMethodActionSheetContainerView.swift in Sources */, 58EFC76E2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift in Sources */, 5827B0B92B14A1C700CCBBA1 /* MethodTestingStatusCellContentConfiguration.swift in Sources */, 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */, diff --git a/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift b/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift index 90f4bf875ae4..0c6c66ed2468 100644 --- a/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift +++ b/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift @@ -34,53 +34,49 @@ class AccessMethodRepository: AccessMethodRepositoryProtocol { } init() { - add([defaultDirectMethod, defaultBridgesMethod]) - } - - func add(_ method: PersistentAccessMethod) { - add([method]) - } - - func add(_ methods: [PersistentAccessMethod]) { var storedMethods = fetchAll() - methods.forEach { method in - guard !storedMethods.contains(where: { $0.id == method.id }) else { return } - storedMethods.append(method) + [defaultDirectMethod, defaultBridgesMethod].forEach { method in + if !storedMethods.contains(where: { $0.id == method.id }) { + storedMethods.append(method) + } } do { try writeApiAccessMethods(storedMethods) } catch { - print("Could not add access method(s): \(methods) \nError: \(error)") + print("Could not update access methods: \(storedMethods) \nError: \(error)") } } - func update(_ method: PersistentAccessMethod) { - var methods = fetchAll() + func save(_ method: PersistentAccessMethod) { + var storedMethods = fetchAll() - guard let index = methods.firstIndex(where: { $0.id == method.id }) else { return } - methods[index] = method + if let index = storedMethods.firstIndex(where: { $0.id == method.id }) { + storedMethods[index] = method + } else { + storedMethods.append(method) + } do { - try writeApiAccessMethods(methods) + try writeApiAccessMethods(storedMethods) } catch { - print("Could not update access method: \(method) \nError: \(error)") + print("Could not update access methods: \(storedMethods) \nError: \(error)") } } func delete(id: UUID) { - var methods = fetchAll() - guard let index = methods.firstIndex(where: { $0.id == id }) else { return } + var storedMethods = fetchAll() + guard let index = storedMethods.firstIndex(where: { $0.id == id }) else { return } // Prevent removing methods that have static UUIDs and are always present. - let method = methods[index] + let method = storedMethods[index] if !method.kind.isPermanent { - methods.remove(at: index) + storedMethods.remove(at: index) } do { - try writeApiAccessMethods(methods) + try writeApiAccessMethods(storedMethods) } catch { print("Could not delete access method with id: \(id) \nError: \(error)") } diff --git a/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepositoryProtocol.swift b/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepositoryProtocol.swift index 213f524bccef..7cc9add5f77c 100644 --- a/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepositoryProtocol.swift +++ b/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepositoryProtocol.swift @@ -13,13 +13,10 @@ protocol AccessMethodRepositoryProtocol { /// Publisher that propagates a snapshot of persistent store upon modifications. var publisher: PassthroughSubject<[PersistentAccessMethod], Never> { get } - /// Add new access method. + /// Persist modified access method locating existing entry by id. Or, add new if id does + /// not exist. /// - Parameter method: persistent access method model. - func add(_ method: PersistentAccessMethod) - - /// Persist modified access method locating existing entry by id. - /// - Parameter method: persistent access method model. - func update(_ method: PersistentAccessMethod) + func save(_ method: PersistentAccessMethod) /// Delete access method by id. /// - Parameter id: an access method id. diff --git a/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift index bf9ad5f03a9a..3da9cab7129c 100644 --- a/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift +++ b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift @@ -32,6 +32,7 @@ class ProxyConfigurationTester: ProxyConfigurationTesterProtocol { } func cancel() { + cancellable?.cancel() cancellable = nil } } diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index 077214fc391f..246ac4149b90 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -159,7 +159,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo presentTOS(animated: animated, completion: completion) case .main: - presentMain(animated: animated, completion: completion) +// presentMain(animated: animated, completion: completion) + presentSettings(route: .apiAccess, animated: false, completion: completion) case .welcome: presentWelcome(animated: animated, completion: completion) diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/AboutViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/AboutViewController.swift index 60a35bc95b04..7eca0d3b9747 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/AboutViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/AboutViewController.swift @@ -8,13 +8,19 @@ import UIKit -/// View controller used for presenting a detailed information on some topic using markdown in a scrollable text view. +/// View controller used for presenting a detailed information on some topic using a scrollable stack view. class AboutViewController: UIViewController { - private let textView = UITextView() - private let markdown: String + private let scrollView = UIScrollView() + private let contentView = UIStackView() + private let header: String? + private let preamble: String? + private let body: [String] + + init(header: String?, preamble: String?, body: [String]) { + self.header = header + self.preamble = preamble + self.body = body - init(markdown: String) { - self.markdown = markdown super.init(nibName: nil, bundle: nil) } @@ -25,20 +31,62 @@ class AboutViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.paragraphSpacing = 16 + view.backgroundColor = .secondaryColor + navigationController?.navigationBar.configureCustomAppeareance() + + setUpContentView() + + scrollView.addConstrainedSubviews([contentView]) { + contentView.pinEdgesToSuperview() + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor) + } + + view.addConstrainedSubviews([scrollView]) { + scrollView.pinEdgesToSuperview() + } + } + + private func setUpContentView() { + contentView.axis = .vertical + contentView.spacing = 15 + contentView.layoutMargins = UIMetrics.contentInsets + contentView.isLayoutMarginsRelativeArrangement = true + + if let header { + let label = UILabel() + + label.text = header + label.font = .systemFont(ofSize: 28, weight: .bold) + label.textColor = .white + label.numberOfLines = 0 + label.textAlignment = .center + + contentView.addArrangedSubview(label) + contentView.setCustomSpacing(32, after: label) + } + + if let preamble { + let label = UILabel() + + label.text = preamble + label.font = .systemFont(ofSize: 18) + label.textColor = .white + label.numberOfLines = 0 + label.textAlignment = .center + + contentView.addArrangedSubview(label) + contentView.setCustomSpacing(24, after: label) + } - let stylingOptions = MarkdownStylingOptions( - font: .systemFont(ofSize: 17), - paragraphStyle: paragraphStyle - ) + for text in body { + let label = UILabel() - textView.attributedText = NSAttributedString(markdownString: markdown, options: stylingOptions) - textView.textContainerInset = UIMetrics.contentInsets - textView.isEditable = false + label.text = text + label.font = .systemFont(ofSize: 15) + label.textColor = .white + label.numberOfLines = 0 - view.addConstrainedSubviews([textView]) { - textView.pinEdgesToSuperview() + contentView.addArrangedSubview(label) } } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift index 1ccf3ec4b141..c2525ac1089a 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift @@ -10,17 +10,17 @@ import Combine import Routing import UIKit -class AddAccessMethodCoordinator: Coordinator, Presentable { +class AddAccessMethodCoordinator: Coordinator, Presentable, Presenting { private let subject: CurrentValueSubject = .init(AccessMethodViewModel()) - var presentedViewController: UIViewController { - navigationController - } - let navigationController: UINavigationController let accessMethodRepo: AccessMethodRepositoryProtocol let proxyConfigurationTester: ProxyConfigurationTesterProtocol + var presentedViewController: UIViewController { + navigationController + } + init( navigationController: UINavigationController, accessMethodRepo: AccessMethodRepositoryProtocol, @@ -32,30 +32,59 @@ class AddAccessMethodCoordinator: Coordinator, Presentable { } func start() { - let controller = AddAccessMethodViewController( + let controller = MethodSettingsViewController( subject: subject, - interactor: AddAccessMethodInteractor( + interactor: EditAccessMethodInteractor( subject: subject, repo: accessMethodRepo, proxyConfigurationTester: proxyConfigurationTester - ) + ), + alertPresenter: AlertPresenter(context: self) ) + + setUpControllerNavigationItem(controller) controller.delegate = self navigationController.pushViewController(controller, animated: false) } -} -extension AddAccessMethodCoordinator: AddAccessMethodViewControllerDelegate { - func controllerDidAdd(_ controller: AddAccessMethodViewController) { - dismiss(animated: true) + private func setUpControllerNavigationItem(_ controller: MethodSettingsViewController) { + controller.navigationItem.prompt = NSLocalizedString( + "METHOD_SETTINGS_NAVIGATION_ADD_PROMPT", + tableName: "APIAccess", + value: "The app will test the method before saving.", + comment: "" + ) + + controller.navigationItem.title = NSLocalizedString( + "METHOD_SETTINGS_NAVIGATION_ADD_TITLE", + tableName: "APIAccess", + value: "Add access method", + comment: "" + ) + + controller.saveBarButton.title = NSLocalizedString( + "METHOD_SETTINGS_NAVIGATION_ADD_BUTTON", + tableName: "APIAccess", + value: "Add", + comment: "" + ) + + controller.navigationItem.leftBarButtonItem = UIBarButtonItem( + systemItem: .cancel, + primaryAction: UIAction(handler: { [weak self] _ in + self?.dismiss(animated: true) + }) + ) } +} - func controllerDidCancel(_ controller: AddAccessMethodViewController) { +extension AddAccessMethodCoordinator: MethodSettingsViewControllerDelegate { + func controllerDidSaveAccessMethod(_ controller: MethodSettingsViewController) { dismiss(animated: true) } - func controllerShouldShowProtocolPicker(_ controller: AddAccessMethodViewController) { + func controllerShouldShowProtocolPicker(_ controller: MethodSettingsViewController) { let picker = AccessMethodProtocolPicker(navigationController: navigationController) picker.present(currentValue: subject.value.method) { [weak self] newMethod in @@ -63,7 +92,7 @@ extension AddAccessMethodCoordinator: AddAccessMethodViewControllerDelegate { } } - func controllerShouldShowShadowsocksCipherPicker(_ controller: AddAccessMethodViewController) { + func controllerShouldShowShadowsocksCipherPicker(_ controller: MethodSettingsViewController) { let picker = ShadowsocksCipherPicker(navigationController: navigationController) picker.present(currentValue: subject.value.shadowsocks.cipher) { [weak self] selectedCipher in diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodInteractor.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodInteractor.swift deleted file mode 100644 index 88143db43b89..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodInteractor.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// AddAccessMethodInteractor.swift -// MullvadVPN -// -// Created by pronebird on 22/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Combine -import Foundation - -struct AddAccessMethodInteractor: AddAccessMethodInteractorProtocol { - let subject: CurrentValueSubject - let repo: AccessMethodRepositoryProtocol - let proxyConfigurationTester: ProxyConfigurationTesterProtocol - - func addMethod() { - guard let persistentMethod = try? subject.value.intoPersistentAccessMethod() else { return } - repo.add(persistentMethod) - } - - func startProxyConfigurationTest(_ completion: ((Bool) -> Void)?) { - guard let config = try? subject.value.intoPersistentProxyConfiguration() else { return } - - let subject = subject - subject.value.testingStatus = .inProgress - - proxyConfigurationTester.start(configuration: config) { error in - let succeeded = error == nil - - subject.value.testingStatus = succeeded ? .succeeded : .failed - - completion?(succeeded) - } - } - - func cancelProxyConfigurationTest() { - subject.value.testingStatus = .initial - - proxyConfigurationTester.cancel() - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodInteractorProtocol.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodInteractorProtocol.swift deleted file mode 100644 index 62e7e6fe017c..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodInteractorProtocol.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// AddAccessMethodInteractorProtocol.swift -// MullvadVPN -// -// Created by pronebird on 14/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -/// The type implementing the interface for persisting the underlying access method view model in the new entry context. -protocol AddAccessMethodInteractorProtocol: ProxyConfigurationInteractorProtocol { - /// Add new access method to the persistent store. - /// - /// - Calling this method multiple times does nothing as the entry with the same identifier cannot be added more than once. - /// - View controllers should only call this method for valid view models, as this method will do nothing if the view model fails validation. - func addMethod() -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodItemIdentifier.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodItemIdentifier.swift deleted file mode 100644 index 4eb34385e552..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodItemIdentifier.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// AddAccessMethodItemIdentifier.swift -// MullvadVPN -// -// Created by pronebird on 14/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -enum AddAccessMethodItemIdentifier: Hashable { - case name - case `protocol` - case proxyConfiguration(ProxyProtocolConfigurationItemIdentifier) - - /// Returns all shadowsocks items wrapped into `ProxyConfigurationItemIdentifier.proxyConfiguration`. - static var allShadowsocksItems: [AddAccessMethodItemIdentifier] { - ShadowsocksItemIdentifier.allCases.map { .proxyConfiguration(.shadowsocks($0)) } - } - - /// Returns all socks items wrapped into `ProxyConfigurationItemIdentifier.proxyConfiguration`. - static func allSocksItems(authenticate: Bool) -> [AddAccessMethodItemIdentifier] { - SocksItemIdentifier.allCases(authenticate: authenticate).map { .proxyConfiguration(.socks($0)) } - } - - /// Cell identifier for the item identifier. - var cellIdentifier: AccessMethodCellReuseIdentifier { - switch self { - case .name: - .textInput - case .protocol: - .textWithDisclosure - case let .proxyConfiguration(item): - item.cellIdentifier - } - } - - /// Whether cell representing the item should be selectable. - var isSelectable: Bool { - switch self { - case .name: - false - case .protocol: - true - case let .proxyConfiguration(item): - item.isSelectable - } - } - - /// The text label for the corresponding cell. - var text: String? { - switch self { - case .name: - NSLocalizedString("NAME", tableName: "APIAccess", value: "Name", comment: "") - case .protocol: - NSLocalizedString("TYPE", tableName: "APIAccess", value: "Type", comment: "") - case .proxyConfiguration: - nil - } - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodSectionIdentifier.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodSectionIdentifier.swift deleted file mode 100644 index 9bd0fe9953d3..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodSectionIdentifier.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// AddAccessMethodSectionIdentifier.swift -// MullvadVPN -// -// Created by pronebird on 14/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -enum AddAccessMethodSectionIdentifier: Hashable { - case name - case `protocol` - case proxyConfiguration - - /// The section name. - var sectionName: String? { - switch self { - case .name: - nil - case .protocol: - NSLocalizedString( - "PROTOCOL_SECTION_TITLE", - tableName: "APIAccess", - value: "Protocol", - comment: "" - ) - case .proxyConfiguration: - NSLocalizedString( - "HOST_CONFIG_SECTION_TITLE", - tableName: "APIAccess", - value: "Host configuration", - comment: "" - ) - } - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodViewController.swift deleted file mode 100644 index 8f8dec50b80e..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodViewController.swift +++ /dev/null @@ -1,374 +0,0 @@ -// -// AddAccessMethodViewController.swift -// MullvadVPN -// -// Created by pronebird on 08/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Combine -import UIKit - -/// The view controller providing the interface for adding new access method. -class AddAccessMethodViewController: UIViewController, UITableViewDelegate { - private let interactor: AddAccessMethodInteractorProtocol - private var validationError: AccessMethodValidationError? - private let viewModelSubject: CurrentValueSubject - private var cancellables = Set() - private var dataSource: UITableViewDiffableDataSource< - AddAccessMethodSectionIdentifier, - AddAccessMethodItemIdentifier - >? - private lazy var cancelBarButton: UIBarButtonItem = { - UIBarButtonItem( - systemItem: .cancel, - primaryAction: UIAction(handler: { [weak self] _ in - self?.onCancel() - }) - ) - }() - - private lazy var addBarButton: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem( - title: NSLocalizedString("ADD_NAVIGATION_BUTTON", tableName: "APIAccess", value: "Add", comment: ""), - primaryAction: UIAction { [weak self] _ in - self?.onAdd() - } - ) - barButtonItem.style = .done - return barButtonItem - }() - - private lazy var sheetPresentation: AccessMethodActionSheetPresentation = { - let sheetPresentation = AccessMethodActionSheetPresentation() - sheetPresentation.delegate = self - return sheetPresentation - }() - - private let contentController = UITableViewController(style: .insetGrouped) - private var tableView: UITableView { contentController.tableView } - - weak var delegate: AddAccessMethodViewControllerDelegate? - - init(subject: CurrentValueSubject, interactor: AddAccessMethodInteractorProtocol) { - self.viewModelSubject = subject - self.interactor = interactor - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.directionalLayoutMargins = UIMetrics.contentLayoutMargins - view.backgroundColor = .secondaryColor - - configureTableView() - configureNavigationItem() - configureDataSource() - } - - // MARK: - UITableViewDelegate - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return nil } - - guard let headerView = tableView - .dequeueReusableView(withIdentifier: AccessMethodHeaderFooterReuseIdentifier.primary) - else { return nil } - - var contentConfiguration = UIListContentConfiguration.mullvadGroupedHeader() - contentConfiguration.text = sectionIdentifier.sectionName - - headerView.contentConfiguration = contentConfiguration - - return headerView - } - - func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath), - itemIdentifier.isSelectable else { return nil } - - return indexPath - } - - func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { - guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return false } - - return itemIdentifier.isSelectable - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) - - switch itemIdentifier { - case .protocol: - showProtocolSelector() - case .proxyConfiguration(.shadowsocks(.cipher)): - showShadowsocksCipher() - default: - break - } - } - - // MARK: - Pickers handling - - private func showProtocolSelector() { - view.endEditing(false) - delegate?.controllerShouldShowProtocolPicker(self) - } - - private func showShadowsocksCipher() { - view.endEditing(false) - delegate?.controllerShouldShowShadowsocksCipherPicker(self) - } - - // MARK: - Cell configuration - - private func dequeueCell(at indexPath: IndexPath, for itemIdentifier: AddAccessMethodItemIdentifier) - -> UITableViewCell { - let cell = tableView.dequeueReusableView(withIdentifier: itemIdentifier.cellIdentifier, for: indexPath) - - if let cell = cell as? DynamicBackgroundConfiguration { - cell.setAutoAdaptingBackgroundConfiguration(.mullvadListGroupedCell(), selectionType: .dimmed) - } - - switch itemIdentifier { - case .name: - configureName(cell, itemIdentifier: itemIdentifier) - case .protocol: - configureProtocol(cell, itemIdentifier: itemIdentifier) - case let .proxyConfiguration(proxyItemIdentifier): - configureProxy(cell, itemIdentifier: proxyItemIdentifier) - } - - return cell - } - - private func configureProxy(_ cell: UITableViewCell, itemIdentifier: ProxyProtocolConfigurationItemIdentifier) { - switch itemIdentifier { - case let .socks(socksItemIdentifier): - let section = SocksSectionHandler(tableStyle: tableView.style, subject: viewModelSubject) - section.configure(cell, itemIdentifier: socksItemIdentifier) - - case let .shadowsocks(shadowsocksItemIdentifier): - let section = ShadowsocksSectionHandler(tableStyle: tableView.style, subject: viewModelSubject) - section.configure(cell, itemIdentifier: shadowsocksItemIdentifier) - } - } - - private func configureName(_ cell: UITableViewCell, itemIdentifier: AddAccessMethodItemIdentifier) { - var contentConfiguration = TextCellContentConfiguration() - contentConfiguration.text = itemIdentifier.text - contentConfiguration.setPlaceholder(type: .optional) - contentConfiguration.textFieldProperties = .withAutoResignAndDoneReturnKey() - contentConfiguration.inputText = viewModelSubject.value.name - contentConfiguration.editingEvents.onChange = viewModelSubject.bindTextAction(to: \.name) - cell.contentConfiguration = contentConfiguration - } - - private func configureProtocol(_ cell: UITableViewCell, itemIdentifier: AddAccessMethodItemIdentifier) { - var contentConfiguration = UIListContentConfiguration.mullvadValueCell(tableStyle: tableView.style) - contentConfiguration.text = itemIdentifier.text - contentConfiguration.secondaryText = viewModelSubject.value.method.localizedDescription - cell.contentConfiguration = contentConfiguration - - if let cell = cell as? CustomCellDisclosureHandling { - cell.disclosureType = .chevron - } - } - - // MARK: - Data source handling - - private func configureDataSource() { - tableView.registerReusableViews(from: AccessMethodCellReuseIdentifier.self) - tableView.registerReusableViews(from: AccessMethodHeaderFooterReuseIdentifier.self) - - dataSource = UITableViewDiffableDataSource( - tableView: tableView, - cellProvider: { [weak self] _, indexPath, itemIdentifier in - self?.dequeueCell(at: indexPath, for: itemIdentifier) - } - ) - - viewModelSubject.withPreviousValue().sink { [weak self] previousValue, newValue in - self?.viewModelDidChange(previousValue: previousValue, newValue: newValue) - } - .store(in: &cancellables) - } - - private func viewModelDidChange(previousValue: AccessMethodViewModel?, newValue: AccessMethodViewModel) { - let animated = view.window != nil - let previousValidationError = validationError - - validate() - updateBarButtons(newValue: newValue) - updateSheet(previousValue: previousValue, newValue: newValue, animated: animated) - updateModalPresentation(newValue: newValue) - updateDataSource( - previousValue: previousValue, - newValue: newValue, - previousValidationError: previousValidationError, - newValidationError: validationError, - animated: animated - ) - } - - private func updateSheet(previousValue: AccessMethodViewModel?, newValue: AccessMethodViewModel, animated: Bool) { - guard previousValue?.testingStatus != newValue.testingStatus else { return } - - switch newValue.testingStatus { - case .initial: - sheetPresentation.hide(animated: animated) - - case .inProgress, .failed, .succeeded: - var presentationConfiguration = AccessMethodActionSheetPresentationConfiguration() - presentationConfiguration.sheetConfiguration.context = .addNew - presentationConfiguration.sheetConfiguration.contentConfiguration.status = newValue.testingStatus - .sheetStatus - sheetPresentation.configuration = presentationConfiguration - - sheetPresentation.show(in: view, animated: animated) - } - } - - private func updateDataSource( - previousValue: AccessMethodViewModel?, - newValue: AccessMethodViewModel, - previousValidationError: AccessMethodValidationError?, - newValidationError: AccessMethodValidationError?, - animated: Bool - ) { - var snapshot = NSDiffableDataSourceSnapshot() - - snapshot.appendSections([.name, .protocol]) - snapshot.appendItems([.name], toSection: .name) - - snapshot.appendItems([.protocol], toSection: .protocol) - // Reconfigure the protocol item on the access method change. - if let previousValue, previousValue.method != newValue.method { - snapshot.reconfigureOrReloadItems([.protocol]) - } - - if newValue.method.hasProxyConfiguration { - snapshot.appendSections([.proxyConfiguration]) - } - - switch newValue.method { - case .direct, .bridges: - break - - case .shadowsocks: - snapshot.appendItems(AddAccessMethodItemIdentifier.allShadowsocksItems, toSection: .proxyConfiguration) - // Reconfigure cipher item on change. - if let previousValue, previousValue.shadowsocks.cipher != newValue.shadowsocks.cipher { - snapshot.reconfigureOrReloadItems([.proxyConfiguration(.shadowsocks(.cipher))]) - } - - case .socks5: - snapshot.appendItems( - AddAccessMethodItemIdentifier.allSocksItems(authenticate: newValue.socks.authenticate), - toSection: .proxyConfiguration - ) - } - - dataSource?.apply(snapshot, animatingDifferences: animated) - } - - // MARK: - Misc - - private func configureTableView() { - tableView.delegate = self - tableView.backgroundColor = .secondaryColor - - view.addConstrainedSubviews([tableView]) { - tableView.pinEdgesToSuperview() - } - - addChild(contentController) - contentController.didMove(toParent: self) - } - - private func configureNavigationItem() { - navigationItem.prompt = NSLocalizedString( - "ADD_METHOD_NAVIGATION_PROMPT", - tableName: "APIAccess", - value: "The app will test the method before adding it.", - comment: "" - ) - navigationItem.title = NSLocalizedString( - "ADD_METHOD_NAVIGATION_TITLE", - tableName: "APIAccess", - value: "Add access method", - comment: "" - ) - navigationItem.leftBarButtonItem = cancelBarButton - navigationItem.rightBarButtonItem = addBarButton - } - - private func validate() { - let validationResult = Result { try viewModelSubject.value.validate() } - validationError = validationResult.error as? AccessMethodValidationError - } - - private func updateBarButtons(newValue: AccessMethodViewModel) { - addBarButton.isEnabled = newValue.testingStatus == .initial && validationError == nil - cancelBarButton.isEnabled = newValue.testingStatus == .initial - } - - private func updateModalPresentation(newValue: AccessMethodViewModel) { - // Prevent swipe gesture when testing or when the sheet offers user actions. - isModalInPresentation = newValue.testingStatus != .initial - } - - private func onAdd() { - view.endEditing(true) - - interactor.startProxyConfigurationTest { [weak self] succeeded in - if succeeded { - self?.addMethodAndNotifyDelegate(afterDelay: true) - } - } - } - - private func onCancel() { - view.endEditing(true) - interactor.cancelProxyConfigurationTest() - - delegate?.controllerDidCancel(self) - } - - /// Tells interactor to add the access method and then notifies the delegate which then dismisses the view controller. - /// - Parameter afterDelay: whether to add a short delay before calling the delegate. - private func addMethodAndNotifyDelegate(afterDelay: Bool) { - interactor.addMethod() - - guard afterDelay else { - sendControllerDidAdd() - return - } - - // Add a short delay to let user see the sheet with successful status before the delegate dismisses the view - // controller. - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in - self?.sendControllerDidAdd() - } - } - - private func sendControllerDidAdd() { - delegate?.controllerDidAdd(self) - } -} - -extension AddAccessMethodViewController: AccessMethodActionSheetPresentationDelegate { - func sheetDidAdd(sheetPresentation: AccessMethodActionSheetPresentation) { - addMethodAndNotifyDelegate(afterDelay: false) - } - - func sheetDidCancel(sheetPresentation: AccessMethodActionSheetPresentation) { - interactor.cancelProxyConfigurationTest() - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodViewControllerDelegate.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodViewControllerDelegate.swift deleted file mode 100644 index 2dadc4cb71ec..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodViewControllerDelegate.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// AddAccessMethodViewControllerDelegate.swift -// MullvadVPN -// -// Created by pronebird on 23/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -protocol AddAccessMethodViewControllerDelegate: AnyObject { - /// The view controller added the API access method. - /// - /// The delegate should consider dismissing the view controller. - /// - /// - Parameter controller: the calling view controller. - func controllerDidAdd(_ controller: AddAccessMethodViewController) - - /// The user cancelled the view controller. - /// - /// The delegate should consider dismissing the view controller. - /// - /// - Parameter controller: the calling view controller. - func controllerDidCancel(_ controller: AddAccessMethodViewController) - - /// The view controller requests the delegate to present the API access method protocol picker. - /// - /// - Parameter controller: the calling view controller. - func controllerShouldShowProtocolPicker(_ controller: AddAccessMethodViewController) - - /// The view controller requests the delegate to present the cipher picker. - /// - /// - Parameter controller: the calling view controller. - func controllerShouldShowShadowsocksCipherPicker(_ controller: AddAccessMethodViewController) -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ButtonCellContentView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ButtonCellContentView.swift index 66d50d119914..c002057ac568 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ButtonCellContentView.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ButtonCellContentView.swift @@ -12,6 +12,9 @@ import UIKit class ButtonCellContentView: UIView, UIContentView { private let button = AppButton() + /// Default cell corner radius in inset grouped table view + private let tableViewCellCornerRadius: CGFloat = 10 + var configuration: UIContentConfiguration { get { actualConfiguration @@ -60,6 +63,7 @@ class ButtonCellContentView: UIView, UIContentView { private func configureButton() { button.setTitle(actualConfiguration.text, for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 17) button.isEnabled = actualConfiguration.isEnabled button.style = actualConfiguration.style button.overrideContentEdgeInsets = true diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/MethodTestingStatusCellContentConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/MethodTestingStatusCellContentConfiguration.swift deleted file mode 100644 index eaa9d07f7df6..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/MethodTestingStatusCellContentConfiguration.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// MethodTestingStatusCellContentConfiguration.swift -// MullvadVPN -// -// Created by pronebird on 27/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -/// Content configuration for presenting the access method testing progress. -struct MethodTestingStatusCellContentConfiguration: UIContentConfiguration, Equatable { - /// Sheet content configuration. - var sheetConfiguration = AccessMethodActionSheetContentConfiguration() - - /// Layout margins. - var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.insetLayoutMargins - - func makeContentView() -> UIView & UIContentView { - return MethodTestingStatusCellContentView(configuration: self) - } - - func updated(for state: UIConfigurationState) -> Self { - return self - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/MethodTestingStatusCellContentView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/MethodTestingStatusCellContentView.swift deleted file mode 100644 index 6c3f1bb68e93..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/MethodTestingStatusCellContentView.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// MethodTestingStatusContentCell.swift -// MullvadVPN -// -// Created by pronebird on 27/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -/// Content view presenting the access method testing progress. -class MethodTestingStatusCellContentView: UIView, UIContentView { - var configuration: UIContentConfiguration { - get { - actualConfiguration - } - set { - guard let newConfiguration = newValue as? MethodTestingStatusCellContentConfiguration, - actualConfiguration != newConfiguration else { return } - - let previousConfiguration = actualConfiguration - actualConfiguration = newConfiguration - - configureSubviews(previousConfiguration: previousConfiguration) - } - } - - private var actualConfiguration: MethodTestingStatusCellContentConfiguration - private let sheetContentView = AccessMethodActionSheetContentView() - - func supports(_ configuration: UIContentConfiguration) -> Bool { - configuration is MethodTestingStatusCellContentConfiguration - } - - init(configuration: MethodTestingStatusCellContentConfiguration) { - actualConfiguration = configuration - - super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 44)) - - configureSubviews() - addSubviews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func addSubviews() { - addConstrainedSubviews([sheetContentView]) { - sheetContentView.pinEdgesToSuperviewMargins() - } - } - - private func configureSubviews(previousConfiguration: MethodTestingStatusCellContentConfiguration? = nil) { - configureLayoutMargins() - configureSheetContentView() - } - - private func configureLayoutMargins() { - directionalLayoutMargins = actualConfiguration.directionalLayoutMargins - } - - private func configureSheetContentView() { - sheetContentView.configuration = actualConfiguration.sheetConfiguration - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/SwitchCellContentConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/SwitchCellContentConfiguration.swift index 766f9b91db1f..55d9c01533d4 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/SwitchCellContentConfiguration.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/SwitchCellContentConfiguration.swift @@ -28,7 +28,7 @@ struct SwitchCellContentConfiguration: UIContentConfiguration, Equatable { var textProperties = TextProperties() /// Content view layout margins. - var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.insetLayoutMargins + var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.apiAccessInsetLayoutMargins func makeContentView() -> UIView & UIContentView { return SwitchCellContentView(configuration: self) diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/SwitchCellContentView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/SwitchCellContentView.swift index 5f50efd035f3..a0a0d66eece6 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/SwitchCellContentView.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/SwitchCellContentView.swift @@ -37,7 +37,7 @@ class SwitchCellContentView: UIView, UIContentView, UITextFieldDelegate { init(configuration: SwitchCellContentConfiguration) { actualConfiguration = configuration - super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 44)) + super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 0)) configureSubviews() addSubviews() @@ -75,13 +75,14 @@ class SwitchCellContentView: UIView, UIContentView, UITextFieldDelegate { private func configureSwitch() { switchContainer.control.isOn = actualConfiguration.isOn + switchContainer.transform = CGAffineTransform(scaleX: 0.85, y: 0.85) } private func addSubviews() { addConstrainedSubviews([textLabel, switchContainer]) { textLabel.pinEdgesToSuperviewMargins(.all().excluding(.trailing)) switchContainer.centerYAnchor.constraint(equalTo: centerYAnchor) - switchContainer.pinEdgeToSuperviewMargin(.trailing(0)) + switchContainer.pinEdgeToSuperview(.trailing(UIMetrics.SettingsCell.apiAccessSwitchCellTrailingMargin)) switchContainer.leadingAnchor.constraint( greaterThanOrEqualToSystemSpacingAfter: textLabel.trailingAnchor, multiplier: 1 diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/TextCellContentConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/TextCellContentConfiguration.swift index 33311ff64fd9..9fc9e43e12fb 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/TextCellContentConfiguration.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/TextCellContentConfiguration.swift @@ -32,7 +32,7 @@ struct TextCellContentConfiguration: UIContentConfiguration, Equatable { var textFieldProperties = TextFieldProperties() /// The content view layout margins. - var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.insetLayoutMargins + var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.apiAccessInsetLayoutMargins func makeContentView() -> UIView & UIContentView { return TextCellContentView(configuration: self) diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift index 92d3a7074b23..e8ba3180b8e8 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift @@ -10,7 +10,7 @@ import Combine import Routing import UIKit -class EditAccessMethodCoordinator: Coordinator { +class EditAccessMethodCoordinator: Coordinator, Presenting { let navigationController: UINavigationController let subject: CurrentValueSubject = .init(AccessMethodViewModel()) let accessMethodRepo: AccessMethodRepositoryProtocol @@ -19,6 +19,10 @@ class EditAccessMethodCoordinator: Coordinator { var onFinish: ((EditAccessMethodCoordinator) -> Void)? + var presentationContext: UIViewController { + navigationController + } + init( navigationController: UINavigationController, accessMethodRepo: AccessMethodRepositoryProtocol, @@ -41,7 +45,12 @@ class EditAccessMethodCoordinator: Coordinator { repo: accessMethodRepo, proxyConfigurationTester: proxyConfigurationTester ) - let controller = EditAccessMethodViewController(subject: subject, interactor: interactor) + + let controller = EditAccessMethodViewController( + subject: subject, + interactor: interactor, + alertPresenter: AlertPresenter(context: self) + ) controller.delegate = self navigationController.pushViewController(controller, animated: true) @@ -49,17 +58,40 @@ class EditAccessMethodCoordinator: Coordinator { } extension EditAccessMethodCoordinator: EditAccessMethodViewControllerDelegate { - func controllerDidSaveAccessMethod(_ controller: EditAccessMethodViewController) { - onFinish?(self) - } - - func controllerShouldShowProxyConfiguration(_ controller: EditAccessMethodViewController) { + func controllerShouldShowMethodSettings(_ controller: EditAccessMethodViewController) { let interactor = EditAccessMethodInteractor( subject: subject, repo: accessMethodRepo, proxyConfigurationTester: proxyConfigurationTester ) - let controller = ProxyConfigurationViewController(subject: subject, interactor: interactor) + + let controller = MethodSettingsViewController( + subject: subject, + interactor: interactor, + alertPresenter: AlertPresenter(context: self) + ) + + controller.navigationItem.prompt = NSLocalizedString( + "METHOD_SETTINGS_NAVIGATION_ADD_PROMPT", + tableName: "APIAccess", + value: "The app will test the method before saving.", + comment: "" + ) + + controller.navigationItem.title = NSLocalizedString( + "METHOD_SETTINGS_NAVIGATION_ADD_TITLE", + tableName: "APIAccess", + value: "Method settings", + comment: "" + ) + + controller.saveBarButton.title = NSLocalizedString( + "METHOD_SETTINGS_NAVIGATION_ADD_BUTTON", + tableName: "APIAccess", + value: "Save", + comment: "" + ) + controller.delegate = self navigationController.pushViewController(controller, animated: true) @@ -70,8 +102,12 @@ extension EditAccessMethodCoordinator: EditAccessMethodViewControllerDelegate { } } -extension EditAccessMethodCoordinator: ProxyConfigurationViewControllerDelegate { - func controllerShouldShowProtocolPicker(_ controller: ProxyConfigurationViewController) { +extension EditAccessMethodCoordinator: MethodSettingsViewControllerDelegate { + func controllerDidSaveAccessMethod(_ controller: MethodSettingsViewController) { + navigationController.popViewController(animated: true) + } + + func controllerShouldShowProtocolPicker(_ controller: MethodSettingsViewController) { let picker = AccessMethodProtocolPicker(navigationController: navigationController) picker.present(currentValue: subject.value.method) { [weak self] newMethod in @@ -79,7 +115,7 @@ extension EditAccessMethodCoordinator: ProxyConfigurationViewControllerDelegate } } - func controllerShouldShowShadowsocksCipherPicker(_ controller: ProxyConfigurationViewController) { + func controllerShouldShowShadowsocksCipherPicker(_ controller: MethodSettingsViewController) { let picker = ShadowsocksCipherPicker(navigationController: navigationController) picker.present(currentValue: subject.value.shadowsocks.cipher) { [weak self] selectedCipher in diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift index ebecc582c013..28d5da1bb76f 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift @@ -17,7 +17,7 @@ struct EditAccessMethodInteractor: EditAccessMethodInteractorProtocol { func saveAccessMethod() { guard let persistentMethod = try? subject.value.intoPersistentAccessMethod() else { return } - repo.update(persistentMethod) + repo.save(persistentMethod) } func deleteAccessMethod() { diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodItemIdentifier.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodItemIdentifier.swift index a45ebdcb714c..1667746f6b14 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodItemIdentifier.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodItemIdentifier.swift @@ -9,23 +9,21 @@ import Foundation enum EditAccessMethodItemIdentifier: Hashable { - case name - case useIfAvailable - case proxyConfiguration + case enableMethod + case methodSettings case testMethod case testingStatus + case cancelTest case deleteMethod /// Cell identifier for the item identifier. var cellIdentifier: AccessMethodCellReuseIdentifier { switch self { - case .name: - .textInput - case .useIfAvailable: + case .enableMethod: .toggle - case .proxyConfiguration: + case .methodSettings: .textWithDisclosure - case .testMethod, .deleteMethod: + case .testMethod, .cancelTest, .deleteMethod: .button case .testingStatus: .testingStatus @@ -35,9 +33,9 @@ enum EditAccessMethodItemIdentifier: Hashable { /// Returns `true` if the cell background should be made transparent. var isClearBackground: Bool { switch self { - case .testMethod, .testingStatus, .deleteMethod: + case .testMethod, .cancelTest, .testingStatus, .deleteMethod: return true - case .name, .useIfAvailable, .proxyConfiguration: + case .enableMethod, .methodSettings: return false } } @@ -45,9 +43,9 @@ enum EditAccessMethodItemIdentifier: Hashable { /// Whether cell representing the item should be selectable. var isSelectable: Bool { switch self { - case .name, .useIfAvailable, .testMethod, .testingStatus, .deleteMethod: + case .enableMethod, .testMethod, .cancelTest, .testingStatus, .deleteMethod: false - case .proxyConfiguration: + case .methodSettings: true } } @@ -55,14 +53,14 @@ enum EditAccessMethodItemIdentifier: Hashable { /// The text label for the corresponding cell. var text: String? { switch self { - case .name: - NSLocalizedString("NAME", tableName: "APIAccess", value: "Name", comment: "") - case .useIfAvailable: - NSLocalizedString("USE_IF_AVAILABLE", tableName: "APIAccess", value: "Use if available", comment: "") - case .proxyConfiguration: - NSLocalizedString("PROXY_CONFIGURATION", tableName: "APIAccess", value: "Proxy configuration", comment: "") + case .enableMethod: + NSLocalizedString("ENABLE_METHOD", tableName: "APIAccess", value: "Enable method", comment: "") + case .methodSettings: + NSLocalizedString("METHOD_SETTINGS", tableName: "APIAccess", value: "Method settings", comment: "") case .testMethod: NSLocalizedString("TEST_METHOD", tableName: "APIAccess", value: "Test method", comment: "") + case .cancelTest: + NSLocalizedString("CANCEL_TEST", tableName: "APIAccess", value: "Cancel", comment: "") case .testingStatus: nil case .deleteMethod: diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodSectionIdentifier.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodSectionIdentifier.swift index 0f62f94041f0..cd8f978113a5 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodSectionIdentifier.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodSectionIdentifier.swift @@ -9,41 +9,34 @@ import Foundation enum EditAccessMethodSectionIdentifier: Hashable { - case name + case enableMethod + case methodSettings case testMethod - case useIfAvailable - case proxyConfiguration + case cancelTest + case testingStatus case deleteMethod /// The section footer text. var sectionFooter: String? { switch self { - case .name, .deleteMethod: - nil - - case .testMethod: + case .enableMethod: NSLocalizedString( - "TEST_METHOD_FOOTER", + "ENABLE_METHOD_FOOTER", tableName: "APIAccess", - value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", + value: "When enabled, the app can try to communicate with a Mullvad API server using this method.", comment: "" ) - case .useIfAvailable: + case .testMethod: NSLocalizedString( - "USE_IF_AVAILABLE_FOOTER", + "TEST_METHOD_FOOTER", tableName: "APIAccess", - value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", + value: "Performs a connection test to a Mullvad API server via this access method.", comment: "" ) - case .proxyConfiguration: - NSLocalizedString( - "PROXY_CONFIGURATION_FOOTER", - tableName: "APIAccess", - value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", - comment: "" - ) + case .methodSettings, .cancelTest, .testingStatus, .deleteMethod: + nil } } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift index d961906925ed..3863e705bb6a 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewController.swift @@ -14,24 +14,25 @@ class EditAccessMethodViewController: UITableViewController { private let subject: CurrentValueSubject private var validationError: AccessMethodValidationError? private let interactor: EditAccessMethodInteractorProtocol + private var alertPresenter: AlertPresenter private var cancellables = Set() + private var dataSource: UITableViewDiffableDataSource< EditAccessMethodSectionIdentifier, EditAccessMethodItemIdentifier >? - private lazy var saveBarButton: UIBarButtonItem = { - let barButton = UIBarButtonItem(systemItem: .save, primaryAction: UIAction { [weak self] _ in - self?.onSave() - }) - barButton.style = .done - return barButton - }() weak var delegate: EditAccessMethodViewControllerDelegate? - init(subject: CurrentValueSubject, interactor: EditAccessMethodInteractorProtocol) { + init( + subject: CurrentValueSubject, + interactor: EditAccessMethodInteractorProtocol, + alertPresenter: AlertPresenter + ) { self.subject = subject self.interactor = interactor + self.alertPresenter = alertPresenter + super.init(style: .insetGrouped) } @@ -44,11 +45,26 @@ class EditAccessMethodViewController: UITableViewController { view.backgroundColor = .secondaryColor tableView.backgroundColor = .secondaryColor + navigationItem.largeTitleDisplayMode = .never + + isModalInPresentation = true configureDataSource() configureNavigationItem() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + subject.value.testingStatus = .initial + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + interactor.cancelProxyConfigurationTest() + } + + // MARK: - UITableViewDelegate + override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return false } @@ -58,8 +74,30 @@ class EditAccessMethodViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return } - if case .proxyConfiguration = itemIdentifier { - delegate?.controllerShouldShowProxyConfiguration(self) + if case .methodSettings = itemIdentifier { + delegate?.controllerShouldShowMethodSettings(self) + } + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UIMetrics.SettingsCell.apiAccessCellHeight + } + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return nil + } + + // Header height shenanigans to avoid extra spacing in testing sections when testing is NOT ongoing. + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return 0 } + + switch sectionIdentifier { + case .enableMethod, .methodSettings, .deleteMethod, .testMethod: + return UITableView.automaticDimension + case .testingStatus: + return subject.value.testingStatus == .initial ? 0 : UITableView.automaticDimension + case .cancelTest: + return 0 } } @@ -71,7 +109,7 @@ class EditAccessMethodViewController: UITableViewController { .dequeueReusableView(withIdentifier: AccessMethodHeaderFooterReuseIdentifier.primary) else { return nil } - var contentConfiguration = UIListContentConfiguration.mullvadGroupedFooter() + var contentConfiguration = UIListContentConfiguration.mullvadGroupedFooter(tableStyle: tableView.style) contentConfiguration.text = sectionFooterText headerView.contentConfiguration = contentConfiguration @@ -79,6 +117,20 @@ class EditAccessMethodViewController: UITableViewController { return headerView } + // Footer height shenanigans to avoid extra spacing in testing sections when testing is NOT ongoing. + override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return 0 } + + switch sectionIdentifier { + case .enableMethod, .methodSettings, .deleteMethod, .testMethod: + return UITableView.automaticDimension + case .testingStatus: + return 0 + case .cancelTest: + return subject.value.testingStatus == .inProgress ? UITableView.automaticDimension : 0 + } + } + // MARK: - Cell configuration private func dequeueCell(at indexPath: IndexPath, for itemIdentifier: EditAccessMethodItemIdentifier) @@ -88,18 +140,18 @@ class EditAccessMethodViewController: UITableViewController { configureBackground(cell: cell, itemIdentifier: itemIdentifier) switch itemIdentifier { - case .name: - configureName(cell, itemIdentifier: itemIdentifier) case .testMethod: configureTestMethod(cell, itemIdentifier: itemIdentifier) + case .cancelTest: + configureCancelTest(cell, itemIdentifier: itemIdentifier) case .testingStatus: configureTestingStatus(cell, itemIdentifier: itemIdentifier) case .deleteMethod: configureDeleteMethod(cell, itemIdentifier: itemIdentifier) - case .useIfAvailable: - configureUseIfAvailable(cell, itemIdentifier: itemIdentifier) - case .proxyConfiguration: - configureProxyConfiguration(cell, itemIdentifier: itemIdentifier) + case .enableMethod: + configureEnableMethod(cell, itemIdentifier: itemIdentifier) + case .methodSettings: + configureMethodSettings(cell, itemIdentifier: itemIdentifier) } return cell @@ -115,7 +167,7 @@ class EditAccessMethodViewController: UITableViewController { var backgroundConfiguration = UIBackgroundConfiguration.mullvadListGroupedCell() - if case .proxyConfiguration = itemIdentifier, let validationError, + if case .methodSettings = itemIdentifier, let validationError, validationError.containsProxyConfigurationErrors(selectedMethod: subject.value.method) { backgroundConfiguration.applyValidationErrorStyle() } @@ -123,34 +175,33 @@ class EditAccessMethodViewController: UITableViewController { cell.setAutoAdaptingBackgroundConfiguration(backgroundConfiguration, selectionType: .dimmed) } - private func configureName(_ cell: UITableViewCell, itemIdentifier: EditAccessMethodItemIdentifier) { - var contentConfiguration = TextCellContentConfiguration() + private func configureTestMethod(_ cell: UITableViewCell, itemIdentifier: EditAccessMethodItemIdentifier) { + var contentConfiguration = ButtonCellContentConfiguration() contentConfiguration.text = itemIdentifier.text - contentConfiguration.setPlaceholder(type: .optional) - contentConfiguration.textFieldProperties = .withAutoResignAndDoneReturnKey() - contentConfiguration.inputText = subject.value.name - contentConfiguration.editingEvents.onChange = subject.bindTextAction(to: \.name) + contentConfiguration.isEnabled = subject.value.testingStatus != .inProgress + contentConfiguration.primaryAction = UIAction { [weak self] _ in + self?.onTest() + } cell.contentConfiguration = contentConfiguration } - private func configureTestMethod(_ cell: UITableViewCell, itemIdentifier: EditAccessMethodItemIdentifier) { + private func configureCancelTest(_ cell: UITableViewCell, itemIdentifier: EditAccessMethodItemIdentifier) { var contentConfiguration = ButtonCellContentConfiguration() - contentConfiguration.style = .tableInsetGroupedSuccess contentConfiguration.text = itemIdentifier.text - contentConfiguration.isEnabled = subject.value.testingStatus != .inProgress + contentConfiguration.isEnabled = subject.value.testingStatus == .inProgress contentConfiguration.primaryAction = UIAction { [weak self] _ in - self?.onTest() + self?.onCancelTest() } cell.contentConfiguration = contentConfiguration } private func configureTestingStatus(_ cell: UITableViewCell, itemIdentifier: EditAccessMethodItemIdentifier) { var contentConfiguration = MethodTestingStatusCellContentConfiguration() - contentConfiguration.sheetConfiguration = .init(status: subject.value.testingStatus.sheetStatus) + contentConfiguration.status = subject.value.testingStatus.viewStatus cell.contentConfiguration = contentConfiguration } - private func configureUseIfAvailable(_ cell: UITableViewCell, itemIdentifier: EditAccessMethodItemIdentifier) { + private func configureEnableMethod(_ cell: UITableViewCell, itemIdentifier: EditAccessMethodItemIdentifier) { var contentConfiguration = SwitchCellContentConfiguration() contentConfiguration.text = itemIdentifier.text contentConfiguration.isOn = subject.value.isEnabled @@ -158,7 +209,7 @@ class EditAccessMethodViewController: UITableViewController { cell.contentConfiguration = contentConfiguration } - private func configureProxyConfiguration(_ cell: UITableViewCell, itemIdentifier: EditAccessMethodItemIdentifier) { + private func configureMethodSettings(_ cell: UITableViewCell, itemIdentifier: EditAccessMethodItemIdentifier) { var contentConfiguration = UIListContentConfiguration.mullvadCell(tableStyle: tableView.style) contentConfiguration.text = itemIdentifier.text cell.contentConfiguration = contentConfiguration @@ -202,7 +253,7 @@ class EditAccessMethodViewController: UITableViewController { let previousValidationError = validationError validateViewModel() - updateBarButtons() + configureNavigationItem() updateDataSource( previousValue: previousValue, newValue: newValue, @@ -221,39 +272,42 @@ class EditAccessMethodViewController: UITableViewController { ) { var snapshot = NSDiffableDataSourceSnapshot() - // Add name field for user-defined access methods. - if !newValue.method.isPermanent { - snapshot.appendSections([.name]) - snapshot.appendItems([.name], toSection: .name) - } + snapshot.appendSections([.enableMethod]) + snapshot.appendItems([.enableMethod], toSection: .enableMethod) - // Add static sections. - snapshot.appendSections([.testMethod, .useIfAvailable]) + // Add method settings if the access method is configurable. + if newValue.method.hasProxyConfiguration { + snapshot.appendSections([.methodSettings]) + snapshot.appendItems([.methodSettings], toSection: .methodSettings) + // Reconfigure the proxy configuration cell if validation error changed. + if previousValidationError != newValidationError { + snapshot.reconfigureOrReloadItems([.methodSettings]) + } + } + + snapshot.appendSections([.testMethod]) snapshot.appendItems([.testMethod], toSection: .testMethod) + // Reconfigure the test button on status changes. if let previousValue, previousValue.testingStatus != newValue.testingStatus { snapshot.reconfigureOrReloadItems([.testMethod]) } + snapshot.appendSections([.testingStatus]) + snapshot.appendSections([.cancelTest]) + // Add test status below the test button. if newValue.testingStatus != .initial { - snapshot.appendItems([.testingStatus], toSection: .testMethod) + snapshot.appendItems([.testingStatus], toSection: .testingStatus) + if let previousValue, previousValue.testingStatus != newValue.testingStatus { snapshot.reconfigureOrReloadItems([.testingStatus]) } - } - - snapshot.appendItems([.useIfAvailable], toSection: .useIfAvailable) - // Add proxy configuration if the access method is configurable. - if newValue.method.hasProxyConfiguration { - snapshot.appendSections([.proxyConfiguration]) - snapshot.appendItems([.proxyConfiguration], toSection: .proxyConfiguration) - - // Reconfigure the proxy configuration cell if validation error changed. - if previousValidationError != newValidationError { - snapshot.reconfigureOrReloadItems([.proxyConfiguration]) + // Show cancel test button below test status. + if newValue.testingStatus == .inProgress { + snapshot.appendItems([.cancelTest], toSection: .cancelTest) } } @@ -270,7 +324,6 @@ class EditAccessMethodViewController: UITableViewController { private func configureNavigationItem() { navigationItem.title = subject.value.navigationItemTitle - navigationItem.rightBarButtonItem = saveBarButton } private func validateViewModel() { @@ -278,21 +331,51 @@ class EditAccessMethodViewController: UITableViewController { validationError = validationResult.error as? AccessMethodValidationError } - private func updateBarButtons() { - saveBarButton.isEnabled = validationError == nil - } - private func onDelete() { - interactor.deleteAccessMethod() - delegate?.controllerDidDeleteAccessMethod(self) - } + let presentation = AlertPresentation( + id: "api-access-methods-delete-method-alert", + icon: .alert, + message: NSLocalizedString( + "METHOD_SETTINGS_SAVE_PROMPT", + tableName: "APIAccess", + value: "Delete \(subject.value.name)?", + comment: "" + ), + buttons: [ + AlertAction( + title: NSLocalizedString( + "METHOD_SETTINGS_DELETE_BUTTON", + tableName: "APIAccess", + value: "Delete", + comment: "" + ), + style: .destructive, + handler: { [weak self] in + guard let self else { return } + interactor.deleteAccessMethod() + delegate?.controllerDidDeleteAccessMethod(self) + } + ), + AlertAction( + title: NSLocalizedString( + "METHOD_SETTINGS_CANCEL_BUTTON", + tableName: "APIAccess", + value: "Cancel", + comment: "" + ), + style: .default + ), + ] + ) - private func onSave() { - interactor.saveAccessMethod() - delegate?.controllerDidSaveAccessMethod(self) + alertPresenter.showAlert(presentation: presentation, animated: true) } private func onTest() { interactor.startProxyConfigurationTest() } + + private func onCancelTest() { + interactor.cancelProxyConfigurationTest() + } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift index 29e0dc48687a..aee945cc76f7 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift @@ -11,7 +11,7 @@ import Foundation protocol EditAccessMethodViewControllerDelegate: AnyObject { /// The view controller requests the delegate to present the proxy configuration view controller. /// - Parameter controller: the calling controller. - func controllerShouldShowProxyConfiguration(_ controller: EditAccessMethodViewController) + func controllerShouldShowMethodSettings(_ controller: EditAccessMethodViewController) /// The view controller deleted the access method. /// @@ -19,11 +19,4 @@ protocol EditAccessMethodViewControllerDelegate: AnyObject { /// /// - Parameter controller: the calling controller. func controllerDidDeleteAccessMethod(_ controller: EditAccessMethodViewController) - - /// The view controller saved changes to the access method. - /// - /// The delegate should consider dismissing the view controller. - /// - /// - Parameter controller: the calling controller. - func controllerDidSaveAccessMethod(_ controller: EditAccessMethodViewController) } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationItemIdentifier.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsItemIdentifier.swift similarity index 58% rename from ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationItemIdentifier.swift rename to ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsItemIdentifier.swift index 68f061584e1d..186432c220be 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationItemIdentifier.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsItemIdentifier.swift @@ -1,5 +1,5 @@ // -// ProxyConfigurationItemIdentifier.swift +// MethodSettingsItemIdentifier.swift // MullvadVPN // // Created by pronebird on 22/11/2023. @@ -8,33 +8,54 @@ import Foundation -enum ProxyConfigurationItemIdentifier: Hashable { +enum MethodSettingsItemIdentifier: Hashable { + case name case `protocol` case proxyConfiguration(ProxyProtocolConfigurationItemIdentifier) + case testingStatus + case cancelTest /// Returns all shadowsocks items wrapped into `ProxyConfigurationItemIdentifier.proxyConfiguration`. - static var allShadowsocksItems: [ProxyConfigurationItemIdentifier] { + static var allShadowsocksItems: [MethodSettingsItemIdentifier] { ShadowsocksItemIdentifier.allCases.map { .proxyConfiguration(.shadowsocks($0)) } } /// Returns all socks items wrapped into `ProxyConfigurationItemIdentifier.proxyConfiguration`. - static func allSocksItems(authenticate: Bool) -> [ProxyConfigurationItemIdentifier] { + static func allSocksItems(authenticate: Bool) -> [MethodSettingsItemIdentifier] { SocksItemIdentifier.allCases(authenticate: authenticate).map { .proxyConfiguration(.socks($0)) } } /// Cell identifiers for the item identifier. var cellIdentifier: AccessMethodCellReuseIdentifier { switch self { + case .name: + .textInput case .protocol: .textWithDisclosure case let .proxyConfiguration(itemIdentifier): itemIdentifier.cellIdentifier + case .testingStatus: + .testingStatus + case .cancelTest: + .button + } + } + + /// Returns `true` if the cell background should be made transparent. + var isClearBackground: Bool { + switch self { + case .cancelTest, .testingStatus: + return true + case .name, .protocol, .proxyConfiguration: + return false } } /// Indicates whether cell representing the item should be selectable. var isSelectable: Bool { switch self { + case .name, .testingStatus, .cancelTest: + false case .protocol: true case let .proxyConfiguration(itemIdentifier): @@ -45,10 +66,16 @@ enum ProxyConfigurationItemIdentifier: Hashable { /// The text label for the corresponding cell. var text: String? { switch self { + case .name: + NSLocalizedString("NAME", tableName: "APIAccess", value: "Name", comment: "") case .protocol: NSLocalizedString("TYPE", tableName: "APIAccess", value: "Type", comment: "") case .proxyConfiguration: nil + case .cancelTest: + NSLocalizedString("CANCEL_TEST", tableName: "APIAccess", value: "Cancel", comment: "") + case .testingStatus: + nil } } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift new file mode 100644 index 000000000000..7bc452330a8a --- /dev/null +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift @@ -0,0 +1,445 @@ +// +// MethodSettingsViewController.swift +// MullvadVPN +// +// Created by pronebird on 21/11/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Combine +import struct MullvadTypes.Duration +import UIKit + +/// The view controller providing the interface for editing method settings +/// and testing the proxy configuration. +class MethodSettingsViewController: UITableViewController { + private let subject: CurrentValueSubject + private let interactor: EditAccessMethodInteractorProtocol + private var cancellables = Set() + private var alertPresenter: AlertPresenter + + private var dataSource: UITableViewDiffableDataSource< + MethodSettingsSectionIdentifier, + MethodSettingsItemIdentifier + >? + + private var isTesting: Bool { + subject.value.testingStatus == .inProgress + } + + lazy var saveBarButton: UIBarButtonItem = { + let barButtonItem = UIBarButtonItem( + title: NSLocalizedString("SAVE_NAVIGATION_BUTTON", tableName: "APIAccess", value: "Save", comment: ""), + primaryAction: UIAction { [weak self] _ in + self?.onTest() + } + ) + barButtonItem.style = .done + return barButtonItem + }() + + weak var delegate: MethodSettingsViewControllerDelegate? + + init( + subject: CurrentValueSubject, + interactor: EditAccessMethodInteractorProtocol, + alertPresenter: AlertPresenter + ) { + self.subject = subject + self.interactor = interactor + self.alertPresenter = alertPresenter + + super.init(style: .insetGrouped) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.directionalLayoutMargins = UIMetrics.contentLayoutMargins + view.backgroundColor = .secondaryColor + + navigationItem.rightBarButtonItem = saveBarButton + isModalInPresentation = true + + subject.value.testingStatus = .initial + + configureTableView() + configureDataSource() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + interactor.cancelProxyConfigurationTest() + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return 0 } + + switch itemIdentifier { + case .name, .protocol, .proxyConfiguration, .cancelTest: + return UIMetrics.SettingsCell.apiAccessCellHeight + case .testingStatus: + return UITableView.automaticDimension + } + } + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return nil } + + guard let headerView = tableView + .dequeueReusableView(withIdentifier: AccessMethodHeaderFooterReuseIdentifier.primary) + else { return nil } + + var contentConfiguration = UIListContentConfiguration.mullvadGroupedHeader(tableStyle: tableView.style) + contentConfiguration.text = sectionIdentifier.sectionName + + headerView.contentConfiguration = contentConfiguration + + return headerView + } + + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return 0 } + + switch sectionIdentifier { + case .name, .protocol, .proxyConfiguration, .testingStatus: + return UITableView.automaticDimension + case .cancelTest: + return 0 + } + } + + override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return nil + } + + override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return 0 } + + switch sectionIdentifier { + case .name, .protocol, .proxyConfiguration, .cancelTest: + return UITableView.automaticDimension + case .testingStatus: + return 0 + } + } + + override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + guard !isTesting, let itemIdentifier = dataSource?.itemIdentifier(for: indexPath), + itemIdentifier.isSelectable else { return nil } + + return indexPath + } + + override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return false } + + return itemIdentifier.isSelectable + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) + + switch itemIdentifier { + case .protocol: + showProtocolSelector() + case .proxyConfiguration(.shadowsocks(.cipher)): + showShadowsocksCipher() + default: + break + } + } + + // MARK: - Pickers handling + + private func showProtocolSelector() { + view.endEditing(false) + delegate?.controllerShouldShowProtocolPicker(self) + } + + private func showShadowsocksCipher() { + view.endEditing(false) + delegate?.controllerShouldShowShadowsocksCipherPicker(self) + } + + // MARK: - Cell configuration + + private func dequeueCell(at indexPath: IndexPath, for itemIdentifier: MethodSettingsItemIdentifier) + -> UITableViewCell { + let cell = tableView.dequeueReusableView(withIdentifier: itemIdentifier.cellIdentifier, for: indexPath) + + if let cell = cell as? DynamicBackgroundConfiguration { + if itemIdentifier.isClearBackground { + cell.setAutoAdaptingClearBackgroundConfiguration() + } else { + cell.setAutoAdaptingBackgroundConfiguration(.mullvadListGroupedCell(), selectionType: .dimmed) + } + } + + switch itemIdentifier { + case .name: + configureName(cell, itemIdentifier: itemIdentifier) + case .protocol: + configureProtocol(cell, itemIdentifier: itemIdentifier) + case let .proxyConfiguration(proxyItemIdentifier): + configureProxy(cell, itemIdentifier: proxyItemIdentifier) + case .testingStatus: + configureTestingStatus(cell, itemIdentifier: itemIdentifier) + case .cancelTest: + configureCancelTest(cell, itemIdentifier: itemIdentifier) + } + + return cell + } + + private func configureName(_ cell: UITableViewCell, itemIdentifier: MethodSettingsItemIdentifier) { + var contentConfiguration = TextCellContentConfiguration() + contentConfiguration.text = itemIdentifier.text + contentConfiguration.setPlaceholder(type: .optional) + contentConfiguration.textFieldProperties = .withAutoResignAndDoneReturnKey() + contentConfiguration.inputText = subject.value.name + contentConfiguration.editingEvents.onChange = subject.bindTextAction(to: \.name) + + cell.setDisabled(isTesting) + cell.contentConfiguration = contentConfiguration + } + + private func configureProxy(_ cell: UITableViewCell, itemIdentifier: ProxyProtocolConfigurationItemIdentifier) { + switch itemIdentifier { + case let .socks(socksItemIdentifier): + let section = SocksSectionHandler(tableStyle: tableView.style, subject: subject) + section.configure(cell, itemIdentifier: socksItemIdentifier) + + case let .shadowsocks(shadowsocksItemIdentifier): + let section = ShadowsocksSectionHandler(tableStyle: tableView.style, subject: subject) + section.configure(cell, itemIdentifier: shadowsocksItemIdentifier) + } + + cell.setDisabled(isTesting) + } + + private func configureProtocol(_ cell: UITableViewCell, itemIdentifier: MethodSettingsItemIdentifier) { + var contentConfiguration = UIListContentConfiguration.mullvadValueCell( + tableStyle: tableView.style, + isEnabled: !isTesting + ) + contentConfiguration.text = itemIdentifier.text + contentConfiguration.secondaryText = subject.value.method.localizedDescription + cell.contentConfiguration = contentConfiguration + + if let cell = cell as? CustomCellDisclosureHandling { + cell.disclosureType = .chevron + } + + cell.setDisabled(isTesting) + } + + private func configureCancelTest(_ cell: UITableViewCell, itemIdentifier: MethodSettingsItemIdentifier) { + var contentConfiguration = ButtonCellContentConfiguration() + contentConfiguration.text = itemIdentifier.text + contentConfiguration.isEnabled = isTesting + contentConfiguration.primaryAction = UIAction { [weak self] _ in + self?.onCancelTest() + } + + cell.contentConfiguration = contentConfiguration + } + + private func configureTestingStatus(_ cell: UITableViewCell, itemIdentifier: MethodSettingsItemIdentifier) { + let viewStatus = subject.value.testingStatus.viewStatus + + var contentConfiguration = MethodTestingStatusCellContentConfiguration() + contentConfiguration.status = viewStatus + contentConfiguration.detailText = viewStatus == .reachable + ? NSLocalizedString( + "METHOD_SETTINGS_SAVING_CHANGES", + tableName: "APIAccess", + value: "Saving changes...", + comment: "" + ) + : nil + + cell.contentConfiguration = contentConfiguration + } + + // MARK: - Data source handling + + private func configureDataSource() { + tableView.registerReusableViews(from: AccessMethodCellReuseIdentifier.self) + tableView.registerReusableViews(from: AccessMethodHeaderFooterReuseIdentifier.self) + + dataSource = UITableViewDiffableDataSource( + tableView: tableView, + cellProvider: { [weak self] _, indexPath, itemIdentifier in + self?.dequeueCell(at: indexPath, for: itemIdentifier) + } + ) + + subject.withPreviousValue() + .sink { [weak self] previousValue, newValue in + self?.viewModelDidChange(previousValue: previousValue, newValue: newValue) + } + .store(in: &cancellables) + } + + private func viewModelDidChange(previousValue: AccessMethodViewModel?, newValue: AccessMethodViewModel) { + let animated = view.window != nil + + validate() + updateDataSource(previousValue: previousValue, newValue: newValue, animated: animated) + onTestCompleted(previousValue: previousValue, newValue: newValue) + } + + private func updateDataSource( + previousValue: AccessMethodViewModel?, + newValue: AccessMethodViewModel, + animated: Bool + ) { + var snapshot = NSDiffableDataSourceSnapshot() + + // Add name field for user-defined access methods. + if !newValue.method.isPermanent { + snapshot.appendSections([.name]) + snapshot.appendItems([.name], toSection: .name) + } + + snapshot.appendSections([.protocol]) + snapshot.appendItems([.protocol], toSection: .protocol) + // Reconfigure protocol cell on change. + if let previousValue, previousValue.method != newValue.method { + snapshot.reconfigureOrReloadItems([.protocol]) + } + + // Add proxy configuration section if the access method is configurable. + if newValue.method.hasProxyConfiguration { + snapshot.appendSections([.proxyConfiguration]) + } + + switch newValue.method { + case .direct, .bridges: + break + + case .shadowsocks: + snapshot.appendItems(MethodSettingsItemIdentifier.allShadowsocksItems, toSection: .proxyConfiguration) + // Reconfigure cipher cell on change. + if let previousValue, previousValue.shadowsocks.cipher != newValue.shadowsocks.cipher { + snapshot.reconfigureOrReloadItems([.proxyConfiguration(.shadowsocks(.cipher))]) + } + + case .socks5: + snapshot.appendItems( + MethodSettingsItemIdentifier.allSocksItems(authenticate: newValue.socks.authenticate), + toSection: .proxyConfiguration + ) + } + + snapshot.appendSections([.testingStatus]) + snapshot.appendSections([.cancelTest]) + + // Add test status below the test button. + if newValue.testingStatus != .initial { + snapshot.appendItems([.testingStatus], toSection: .testingStatus) + + // Show cancel test button below test status. + if newValue.testingStatus == .inProgress { + snapshot.appendItems([.cancelTest], toSection: .cancelTest) + } + } + + if let previousValue, previousValue.testingStatus != newValue.testingStatus { + snapshot.reconfigureOrReloadItems(snapshot.itemIdentifiers) + } + + dataSource?.apply(snapshot, animatingDifferences: animated) + } + + private func validate() { + let validationResult = Result { try subject.value.validate() } + saveBarButton.isEnabled = validationResult.isSuccess && !isTesting + } + + // MARK: - Misc + + private func configureTableView() { + tableView.delegate = self + tableView.backgroundColor = .secondaryColor + tableView.separatorColor = .secondaryColor + tableView.separatorInset.left = UIMetrics.SettingsCell.apiAccessInsetLayoutMargins.leading + } + + private func onSave(transitionDelay: Duration = .zero) { + saveBarButton.isEnabled = false + interactor.saveAccessMethod() + + DispatchQueue.main.asyncAfter(deadline: .now() + transitionDelay.timeInterval) { [weak self] in + guard let self else { return } + delegate?.controllerDidSaveAccessMethod(self) + } + } + + private func onTest() { + view.endEditing(true) + interactor.startProxyConfigurationTest() + } + + private func onTestCompleted( + previousValue: AccessMethodViewModel?, + newValue: AccessMethodViewModel + ) { + guard previousValue?.testingStatus != newValue.testingStatus else { return } + + switch newValue.testingStatus { + case .initial, .inProgress: + break + + case .failed: + let presentation = AlertPresentation( + id: "api-access-methods-testing-status-failed-alert", + icon: .info, + message: NSLocalizedString( + "METHOD_SETTINGS_SAVE_PROMPT", + tableName: "APIAccess", + value: "API could not be reached, save anyway?", + comment: "" + ), + buttons: [ + AlertAction( + title: NSLocalizedString( + "METHOD_SETTINGS_SAVE_BUTTON", + tableName: "APIAccess", + value: "Save anyway", + comment: "" + ), + style: .default, + handler: { [weak self] in + self?.onSave() + } + ), + AlertAction( + title: NSLocalizedString( + "METHOD_SETTINGS_BACK_BUTTON", + tableName: "APIAccess", + value: "Back to editing", + comment: "" + ), + style: .default + ), + ] + ) + + alertPresenter.showAlert(presentation: presentation, animated: true) + case .succeeded: + onSave(transitionDelay: .seconds(1)) + } + } + + private func onCancelTest() { + interactor.cancelProxyConfigurationTest() + } +} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewControllerDelegate.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewControllerDelegate.swift new file mode 100644 index 000000000000..e50692e42d8e --- /dev/null +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewControllerDelegate.swift @@ -0,0 +1,15 @@ +// +// MethodSettingsViewControllerDelegate.swift +// MullvadVPN +// +// Created by pronebird on 23/11/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol MethodSettingsViewControllerDelegate: AnyObject { + func controllerDidSaveAccessMethod(_ controller: MethodSettingsViewController) + func controllerShouldShowProtocolPicker(_ controller: MethodSettingsViewController) + func controllerShouldShowShadowsocksCipherPicker(_ controller: MethodSettingsViewController) +} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetContentConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodTestingStatusCellContentConfiguration.swift similarity index 63% rename from ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetContentConfiguration.swift rename to ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodTestingStatusCellContentConfiguration.swift index 5fb2de4a9abf..fd759c33fd49 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetContentConfiguration.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodTestingStatusCellContentConfiguration.swift @@ -1,15 +1,15 @@ // -// AccessMethodActionSheetContentConfiguration.swift +// MethodTestingStatusCellContentConfiguration.swift // MullvadVPN // -// Created by pronebird on 28/11/2023. +// Created by pronebird on 27/11/2023. // Copyright © 2023 Mullvad VPN AB. All rights reserved. // import UIKit -/// Sheet content view configuration. -struct AccessMethodActionSheetContentConfiguration: Equatable { +/// Content configuration for presenting the access method testing progress. +struct MethodTestingStatusCellContentConfiguration: UIContentConfiguration, Equatable { /// The status of access method testing. enum Status: Equatable { /// API Is reachable. @@ -27,9 +27,20 @@ struct AccessMethodActionSheetContentConfiguration: Equatable { /// Detail text displayed below the status when set. var detailText: String? + + /// Layout margins. + var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.apiAccessInsetLayoutMargins + + func makeContentView() -> UIView & UIContentView { + return MethodTestingStatusCellContentView(configuration: self) + } + + func updated(for state: UIConfigurationState) -> Self { + return self + } } -extension AccessMethodActionSheetContentConfiguration.Status { +extension MethodTestingStatusCellContentConfiguration.Status { /// The text label descirbing the status of testing and suitable for user presentation. var text: String { switch self { @@ -46,9 +57,9 @@ extension AccessMethodActionSheetContentConfiguration.Status { var statusColor: UIColor? { switch self { case .unreachable: - .dangerColor + .dangerColor case .reachable: - .successColor + .successColor case .testing: nil } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetContentView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodTestingStatusCellContentView.swift similarity index 68% rename from ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetContentView.swift rename to ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodTestingStatusCellContentView.swift index 45579d680cd6..2ef5b8746160 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetContentView.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodTestingStatusCellContentView.swift @@ -1,23 +1,18 @@ // -// AccessMethodActionSheetContentView.swift +// MethodTestingStatusContentCell.swift // MullvadVPN // -// Created by pronebird on 16/11/2023. +// Created by pronebird on 27/11/2023. // Copyright © 2023 Mullvad VPN AB. All rights reserved. // import UIKit -/// The sheet content view implementing a layout with an activity indicator or status indicator and primary text label, with detail label below. -class AccessMethodActionSheetContentView: UIView { - var configuration = AccessMethodActionSheetContentConfiguration() { - didSet { - updateView() - } - } - +/// Content view presenting the access method testing progress. +class MethodTestingStatusCellContentView: UIView, UIContentView { private let progressView = SpinnerActivityIndicatorView(style: .custom) private let progressContainer = UIView() + private let containerView = UIView() private let statusIndicator: UIView = { let view = UIView() @@ -58,20 +53,40 @@ class AccessMethodActionSheetContentView: UIView { return stackView }() - private let containerView = UIView() + var configuration: UIContentConfiguration { + get { + actualConfiguration + } + set { + guard let newConfiguration = newValue as? MethodTestingStatusCellContentConfiguration else { return } + + let previousConfiguration = actualConfiguration + actualConfiguration = newConfiguration + + configureSubviews(previousConfiguration: previousConfiguration) + } + } + + private var actualConfiguration: MethodTestingStatusCellContentConfiguration + + func supports(_ configuration: UIContentConfiguration) -> Bool { + configuration is MethodTestingStatusCellContentConfiguration + } + + init(configuration: MethodTestingStatusCellContentConfiguration) { + actualConfiguration = configuration - init() { super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 44)) - setupView() - updateView() + addSubviews() + configureSubviews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func setupView() { + private func addSubviews() { NSLayoutConstraint.activate([ progressView.widthAnchor.constraint(equalToConstant: 30), progressView.heightAnchor.constraint(equalToConstant: 30), @@ -80,7 +95,7 @@ class AccessMethodActionSheetContentView: UIView { progressContainer.heightAnchor.constraint(equalToConstant: 20), statusIndicator.widthAnchor.constraint(equalToConstant: 20), - statusIndicator.heightAnchor.constraint(equalToConstant: 20), + statusIndicator.heightAnchor.constraint(equalToConstant: 20).withPriority(.defaultHigh), ]) containerView.addConstrainedSubviews([horizontalStackView]) { @@ -94,17 +109,16 @@ class AccessMethodActionSheetContentView: UIView { } addConstrainedSubviews([verticalStackView]) { - verticalStackView.pinEdgesToSuperview() + verticalStackView.pinEdgesToSuperviewMargins() } } - private func updateView() { - textLabel.text = configuration.status.text - detailLabel.text = configuration.detailText - statusIndicator.backgroundColor = configuration.status.statusColor + private func configureSubviews(previousConfiguration: MethodTestingStatusCellContentConfiguration? = nil) { + configureLayoutMargins() - // Hide detail label when empty to prevent extra margin between subviews in the stack. - detailLabel.isHidden = configuration.detailText?.isEmpty ?? true + textLabel.text = actualConfiguration.status.text + detailLabel.text = actualConfiguration.detailText + statusIndicator.backgroundColor = actualConfiguration.status.statusColor // Remove the first view in the horizontal stack which is either a status indicator or progress. horizontalStackView.arrangedSubviews.first.map { view in @@ -113,7 +127,7 @@ class AccessMethodActionSheetContentView: UIView { } // Reconfigure the horizontal stack by adding the status indicator or progress first. - switch configuration.status { + switch actualConfiguration.status { case .reachable, .unreachable: horizontalStackView.insertArrangedSubview(statusIndicator, at: 0) @@ -127,4 +141,8 @@ class AccessMethodActionSheetContentView: UIView { horizontalStackView.addArrangedSubview(textLabel) } } + + private func configureLayoutMargins() { + directionalLayoutMargins = actualConfiguration.directionalLayoutMargins + } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationSectionIdentifier.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/ProxyConfigurationSectionIdentifier.swift similarity index 61% rename from ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationSectionIdentifier.swift rename to ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/ProxyConfigurationSectionIdentifier.swift index 7c02cb33ad7a..431a3a68161e 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationSectionIdentifier.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/ProxyConfigurationSectionIdentifier.swift @@ -1,5 +1,5 @@ // -// ProxyConfigurationSectionIdentifier.swift +// MethodSettingsSectionIdentifier.swift // MullvadVPN // // Created by pronebird on 21/11/2023. @@ -8,19 +8,14 @@ import Foundation -enum ProxyConfigurationSectionIdentifier: Hashable { +enum MethodSettingsSectionIdentifier: Hashable { case `protocol` case proxyConfiguration var sectionName: String? { switch self { - case .protocol: - NSLocalizedString( - "PROTOCOL_SECTION_TITLE", - tableName: "APIAccess", - value: "Protocol", - comment: "" - ) + case .name, .protocol: + nil case .proxyConfiguration: NSLocalizedString( "HOST_CONFIG_SECTION_TITLE", diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfigurationSectionIdentifier.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettingsSectionIdentifier.swift similarity index 55% rename from ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfigurationSectionIdentifier.swift rename to ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettingsSectionIdentifier.swift index 7c02cb33ad7a..2ab1a9689c0a 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfigurationSectionIdentifier.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettingsSectionIdentifier.swift @@ -1,5 +1,5 @@ // -// ProxyConfigurationSectionIdentifier.swift +// MethodSettingsSectionIdentifier.swift // MullvadVPN // // Created by pronebird on 21/11/2023. @@ -8,24 +8,22 @@ import Foundation -enum ProxyConfigurationSectionIdentifier: Hashable { +enum MethodSettingsSectionIdentifier: Hashable { + case name case `protocol` case proxyConfiguration + case testingStatus + case cancelTest var sectionName: String? { switch self { - case .protocol: - NSLocalizedString( - "PROTOCOL_SECTION_TITLE", - tableName: "APIAccess", - value: "Protocol", - comment: "" - ) + case .name, .protocol, .testingStatus, .cancelTest: + nil case .proxyConfiguration: NSLocalizedString( "HOST_CONFIG_SECTION_TITLE", tableName: "APIAccess", - value: "Host configuration", + value: "Server details", comment: "" ) } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationViewController.swift deleted file mode 100644 index 63ce73365e66..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationViewController.swift +++ /dev/null @@ -1,313 +0,0 @@ -// -// ProxyConfigurationViewController.swift -// MullvadVPN -// -// Created by pronebird on 21/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Combine -import UIKit - -/// The view controller providing the interface for editing and testing the proxy configuration. -class ProxyConfigurationViewController: UIViewController, UITableViewDelegate { - private let subject: CurrentValueSubject - private let interactor: ProxyConfigurationInteractorProtocol - private var cancellables = Set() - - private var dataSource: UITableViewDiffableDataSource< - ProxyConfigurationSectionIdentifier, - ProxyConfigurationItemIdentifier - >? - private lazy var testBarButton: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem( - title: NSLocalizedString("TEST_NAVIGATION_BUTTON", tableName: "APIAccess", value: "Test", comment: ""), - primaryAction: UIAction { [weak self] _ in - self?.onTest() - } - ) - barButtonItem.style = .done - return barButtonItem - }() - - private let contentController = UITableViewController(style: .insetGrouped) - private var tableView: UITableView { - contentController.tableView - } - - private lazy var sheetPresentation: AccessMethodActionSheetPresentation = { - let sheetPresentation = AccessMethodActionSheetPresentation() - sheetPresentation.delegate = self - return sheetPresentation - }() - - weak var delegate: ProxyConfigurationViewControllerDelegate? - - init(subject: CurrentValueSubject, interactor: ProxyConfigurationInteractorProtocol) { - self.subject = subject - self.interactor = interactor - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.directionalLayoutMargins = UIMetrics.contentLayoutMargins - view.backgroundColor = .secondaryColor - - configureTableView() - configureNavigationItem() - configureDataSource() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - updateTableSafeAreaInsets() - } - - // MARK: - UITableViewDelegate - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let sectionIdentifier = dataSource?.snapshot().sectionIdentifiers[section] else { return nil } - - guard let headerView = tableView - .dequeueReusableView(withIdentifier: AccessMethodHeaderFooterReuseIdentifier.primary) - else { return nil } - - var contentConfiguration = UIListContentConfiguration.mullvadGroupedHeader() - contentConfiguration.text = sectionIdentifier.sectionName - - headerView.contentConfiguration = contentConfiguration - - return headerView - } - - func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath), - itemIdentifier.isSelectable else { return nil } - - return indexPath - } - - func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { - guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return false } - - return itemIdentifier.isSelectable - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) - - switch itemIdentifier { - case .protocol: - showProtocolSelector() - case .proxyConfiguration(.shadowsocks(.cipher)): - showShadowsocksCipher() - default: - break - } - } - - // MARK: - Pickers handling - - private func showProtocolSelector() { - view.endEditing(false) - delegate?.controllerShouldShowProtocolPicker(self) - } - - private func showShadowsocksCipher() { - view.endEditing(false) - delegate?.controllerShouldShowShadowsocksCipherPicker(self) - } - - // MARK: - Cell configuration - - private func dequeueCell(at indexPath: IndexPath, for itemIdentifier: ProxyConfigurationItemIdentifier) - -> UITableViewCell { - let cell = tableView.dequeueReusableView(withIdentifier: itemIdentifier.cellIdentifier, for: indexPath) - - if let cell = cell as? DynamicBackgroundConfiguration { - cell.setAutoAdaptingBackgroundConfiguration(.mullvadListGroupedCell(), selectionType: .dimmed) - } - - switch itemIdentifier { - case .protocol: - configureProtocol(cell, itemIdentifier: itemIdentifier) - case let .proxyConfiguration(proxyItemIdentifier): - configureProxy(cell, itemIdentifier: proxyItemIdentifier) - } - - return cell - } - - private func configureProxy(_ cell: UITableViewCell, itemIdentifier: ProxyProtocolConfigurationItemIdentifier) { - switch itemIdentifier { - case let .socks(socksItemIdentifier): - let section = SocksSectionHandler(tableStyle: tableView.style, subject: subject) - section.configure(cell, itemIdentifier: socksItemIdentifier) - - case let .shadowsocks(shadowsocksItemIdentifier): - let section = ShadowsocksSectionHandler(tableStyle: tableView.style, subject: subject) - section.configure(cell, itemIdentifier: shadowsocksItemIdentifier) - } - } - - private func configureProtocol(_ cell: UITableViewCell, itemIdentifier: ProxyConfigurationItemIdentifier) { - var contentConfiguration = UIListContentConfiguration.mullvadValueCell(tableStyle: tableView.style) - contentConfiguration.text = itemIdentifier.text - contentConfiguration.secondaryText = subject.value.method.localizedDescription - cell.contentConfiguration = contentConfiguration - - if let cell = cell as? CustomCellDisclosureHandling { - cell.disclosureType = .chevron - } - } - - // MARK: - Data source handling - - private func configureDataSource() { - tableView.registerReusableViews(from: AccessMethodCellReuseIdentifier.self) - tableView.registerReusableViews(from: AccessMethodHeaderFooterReuseIdentifier.self) - - dataSource = UITableViewDiffableDataSource( - tableView: tableView, - cellProvider: { [weak self] _, indexPath, itemIdentifier in - self?.dequeueCell(at: indexPath, for: itemIdentifier) - } - ) - - subject.withPreviousValue() - .sink { [weak self] previousValue, newValue in - self?.viewModelDidChange(previousValue: previousValue, newValue: newValue) - } - .store(in: &cancellables) - } - - private func viewModelDidChange(previousValue: AccessMethodViewModel?, newValue: AccessMethodViewModel) { - let animated = view.window != nil - - updateDataSource(previousValue: previousValue, newValue: newValue, animated: animated) - updateSheet(previousValue: previousValue, newValue: newValue, animated: animated) - validate() - } - - private func updateSheet(previousValue: AccessMethodViewModel?, newValue: AccessMethodViewModel, animated: Bool) { - guard previousValue?.testingStatus != newValue.testingStatus else { return } - - switch newValue.testingStatus { - case .initial: - sheetPresentation.hide(animated: animated) - - case .inProgress, .failed, .succeeded: - var presentationConfiguration = AccessMethodActionSheetPresentationConfiguration() - presentationConfiguration.dimsBackground = newValue.testingStatus == .inProgress - presentationConfiguration.sheetConfiguration.context = .proxyConfiguration - presentationConfiguration.sheetConfiguration.contentConfiguration.status = newValue.testingStatus - .sheetStatus - sheetPresentation.configuration = presentationConfiguration - - sheetPresentation.show(in: view, animated: animated) - } - } - - private func updateDataSource( - previousValue: AccessMethodViewModel?, - newValue: AccessMethodViewModel, - animated: Bool - ) { - var snapshot = NSDiffableDataSourceSnapshot< - ProxyConfigurationSectionIdentifier, - ProxyConfigurationItemIdentifier - >() - - snapshot.appendSections([.protocol]) - snapshot.appendItems([.protocol], toSection: .protocol) - // Reconfigure protocol cell on change. - if let previousValue, previousValue.method != newValue.method { - snapshot.reconfigureOrReloadItems([.protocol]) - } - - // Add proxy configuration section if the access method is configurable. - if newValue.method.hasProxyConfiguration { - snapshot.appendSections([.proxyConfiguration]) - } - - switch newValue.method { - case .direct, .bridges: - break - - case .shadowsocks: - snapshot.appendItems(ProxyConfigurationItemIdentifier.allShadowsocksItems, toSection: .proxyConfiguration) - // Reconfigure cipher cell on change. - if let previousValue, previousValue.shadowsocks.cipher != newValue.shadowsocks.cipher { - snapshot.reconfigureOrReloadItems([.proxyConfiguration(.shadowsocks(.cipher))]) - } - - case .socks5: - snapshot.appendItems( - ProxyConfigurationItemIdentifier.allSocksItems(authenticate: newValue.socks.authenticate), - toSection: .proxyConfiguration - ) - } - - dataSource?.apply(snapshot, animatingDifferences: animated) - } - - private func validate() { - let validationResult = Result { try subject.value.validate() } - testBarButton.isEnabled = validationResult.isSuccess && subject.value.testingStatus != .inProgress - } - - // MARK: - Misc - - private func configureTableView() { - tableView.delegate = self - tableView.backgroundColor = .secondaryColor - - view.addConstrainedSubviews([tableView]) { - tableView.pinEdgesToSuperview() - } - - addChild(contentController) - contentController.didMove(toParent: self) - } - - private func configureNavigationItem() { - navigationItem.title = NSLocalizedString( - "PROXY_CONFIGURATION_NAVIGATION_TITLE", - tableName: "APIAccess", - value: "Proxy configuration", - comment: "" - ) - navigationItem.rightBarButtonItem = testBarButton - } - - /// Update table view controller safe area to make space for the sheet at the bottom. - private func updateTableSafeAreaInsets() { - let sheetHeight = sheetPresentation.isPresenting ? sheetPresentation.sheetLayoutFrame.height : 0 - var insets = contentController.additionalSafeAreaInsets - // Prevent mutating insets if they haven't changed, in case UIKit doesn't filter duplicates. - if insets.bottom != sheetHeight { - insets.bottom = sheetHeight - contentController.additionalSafeAreaInsets = insets - } - } - - private func onTest() { - view.endEditing(true) - interactor.startProxyConfigurationTest() - } -} - -extension ProxyConfigurationViewController: AccessMethodActionSheetPresentationDelegate { - func sheetDidAdd(sheetPresentation: AccessMethodActionSheetPresentation) {} - - func sheetDidCancel(sheetPresentation: AccessMethodActionSheetPresentation) { - interactor.cancelProxyConfigurationTest() - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationViewControllerDelegate.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationViewControllerDelegate.swift deleted file mode 100644 index 4948b7d4c351..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfiguration/ProxyConfigurationViewControllerDelegate.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// ProxyConfigurationViewControllerDelegate.swift -// MullvadVPN -// -// Created by pronebird on 23/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -protocol ProxyConfigurationViewControllerDelegate: AnyObject { - func controllerShouldShowProtocolPicker(_ controller: ProxyConfigurationViewController) - func controllerShouldShowShadowsocksCipherPicker(_ controller: ProxyConfigurationViewController) -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift index 7b58a4ae52ee..64be07ff5ce9 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift @@ -67,21 +67,45 @@ class ListAccessMethodCoordinator: Coordinator, Presenting, SettingsChildCoordin } private func about() { - // swiftlint:disable line_length - let aboutMarkdown = """ - **What is Lorem Ipsum?** - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. - """ - // swiftlint:enable line_length - - let aboutController = AboutViewController(markdown: aboutMarkdown) - let aboutNavController = UINavigationController(rootViewController: aboutController) - - aboutController.navigationItem.title = NSLocalizedString( - "ABOUT_API_ACCESS_NAV_TITLE", - value: "About API access", + let header = NSLocalizedString( + "ABOUT_API_ACCESS_HEADER", + value: "API access", comment: "" ) + let preamble = NSLocalizedString( + "ABOUT_API_ACCESS_PREAMBLE", + value: "Manage default and setup custom methods to access the Mullvad API.", + comment: "" + ) + let body = [ + NSLocalizedString( + "ABOUT_API_ACCESS_BODY_1", + value: """ + The app needs to communicate with a Mullvad API server to log you in, fetch server lists, \ + and other critical operations. + """, + comment: "" + ), + NSLocalizedString( + "ABOUT_API_ACCESS_BODY_2", + value: """ + On some networks, where various types of censorship are being used, the API servers might \ + not be directly reachable. + """, + comment: "" + ), + NSLocalizedString( + "ABOUT_API_ACCESS_BODY_3", + value: """ + This feature allows you to circumvent that censorship by adding custom ways to access the \ + API via proxies and similar methods. + """, + comment: "" + ), + ] + + let aboutController = AboutViewController(header: header, preamble: preamble, body: body) + let aboutNavController = UINavigationController(rootViewController: aboutController) aboutController.navigationItem.rightBarButtonItem = UIBarButtonItem( systemItem: .done, diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodHeaderView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodHeaderView.swift index 039946bb7fd4..a65ceaea7924 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodHeaderView.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodHeaderView.swift @@ -27,6 +27,7 @@ class ListAccessMethodHeaderView: UIView, UITextViewDelegate { textView.textContainerInset = .zero textView.attributedText = makeAttributedString() textView.linkTextAttributes = defaultLinkAttributes + textView.textContainer.lineFragmentPadding = 0 textView.delegate = self directionalLayoutMargins = UIMetrics.contentHeadingLayoutMargins @@ -39,12 +40,12 @@ class ListAccessMethodHeaderView: UIView, UITextViewDelegate { } private let defaultTextAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.systemFont(ofSize: 17), + .font: UIFont.systemFont(ofSize: 13), .foregroundColor: UIColor.ContentHeading.textColor, ] private let defaultLinkAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.systemFont(ofSize: 17), + .font: UIFont.systemFont(ofSize: 13), .foregroundColor: UIColor.ContentHeading.linkColor, ] diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift index 6d8ba9b92dae..67353b23dd86 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift @@ -58,8 +58,7 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { tableView.delegate = self tableView.backgroundColor = .secondaryColor tableView.separatorColor = .secondaryColor - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 60 + tableView.separatorInset = .zero tableView.registerReusableViews(from: CellReuseIdentifier.self) @@ -81,6 +80,35 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { configureDataSource() } + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + let container = UIView() + + let button = AppButton(style: .tableInsetGroupedDefault) + button.setTitle( + NSLocalizedString( + "LIST_ACCESS_METHODS_ADD_BUTTON", + tableName: "APIAccess", + value: "Add", + comment: "" + ), + for: .normal + ) + button.addAction(UIAction { [weak self] _ in + self?.sendAddNew() + }, for: .touchUpInside) + + let fontSize = button.titleLabel?.font.pointSize ?? 0 + button.titleLabel?.font = UIFont.systemFont(ofSize: fontSize, weight: .regular) + + container.addConstrainedSubviews([button]) { + button.pinEdgesToSuperview(.init([.top(40), .trailing(16), .bottom(0), .leading(16)])) + } + + container.directionalLayoutMargins = UIMetrics.SettingsCell.apiAccessInsetLayoutMargins + + return container + } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let item = fetchedItems[indexPath.row] sendEdit(item: item) @@ -93,12 +121,6 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { value: "API access", comment: "" ) - navigationItem.rightBarButtonItem = UIBarButtonItem( - systemItem: .add, - primaryAction: UIAction(handler: { [weak self] _ in - self?.sendAddNew() - }) - ) } private func configureDataSource() { diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessViewModel+TestingStatus.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessViewModel+TestingStatus.swift index d0544cf8d91a..909629e9ac67 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessViewModel+TestingStatus.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessViewModel+TestingStatus.swift @@ -9,7 +9,7 @@ import Foundation extension AccessMethodViewModel.TestingStatus { - var sheetStatus: AccessMethodActionSheetContentConfiguration.Status { + var viewStatus: MethodTestingStatusCellContentConfiguration.Status { switch self { case .initial: // The sheet is invisible in this state, the return value is not important. diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ListItemPickerViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ListItemPickerViewController.swift index f30f75e2c5fe..0d677b55484e 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ListItemPickerViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ListItemPickerViewController.swift @@ -52,7 +52,7 @@ class ListItemPickerViewController: UITa self.dataSource = dataSource self.selectedItemID = selectedItemID - super.init(style: .insetGrouped) + super.init(style: .plain) } required init?(coder: NSCoder) { @@ -63,7 +63,14 @@ class ListItemPickerViewController: UITa super.viewDidLoad() view.backgroundColor = .secondaryColor + + tableView.separatorInset = .zero + tableView.separatorColor = .secondaryColor tableView.registerReusableViews(from: CellIdentifier.self) + + // Add extra inset to mimic built-in margin of a grouped table view. Without this the + // transition between a plain and a grouped table view looks jarring. + tableView.contentInset.top = UIMetrics.SettingsCell.apiAccessPickerListContentInsetTop } override func viewIsAppearing(_ animated: Bool) { @@ -80,7 +87,7 @@ class ListItemPickerViewController: UITa override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let item = dataSource.item(at: indexPath) - var configuration = UIListContentConfiguration.mullvadCell(tableStyle: tableView.style) + var configuration = UIListContentConfiguration.mullvadCell(tableStyle: .insetGrouped) configuration.text = item.text let cell = tableView.dequeueReusableView(withIdentifier: CellIdentifier.default, for: indexPath) @@ -101,6 +108,10 @@ class ListItemPickerViewController: UITa return dataSource.itemCount } + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UIMetrics.SettingsCell.apiAccessCellHeight + } + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selectedItem = dataSource.item(at: indexPath) selectedItemID = selectedItem.id diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetConfiguration.swift deleted file mode 100644 index 7704bcc3ee1f..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetConfiguration.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// AccessMethodActionSheetConfiguration.swift -// MullvadVPN -// -// Created by pronebird on 28/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -/// The context in which the sheet is being used. -enum AccessMethodActionSheetContext: Equatable { - /// The variant describing the context when adding a new method. - /// - /// In this context, the sheet offers user to add access method anyway or cancel, once the API tests indicate a failure. - /// (See `contentConfiguration.status`) - case addNew - - /// The variant describing the context when the existing API method is being tested or edited as a part of proxy configuration sub-navigation. - /// - /// In this context, the sheet only offers user to cancel testing the access method. - case proxyConfiguration -} - -/// The sheet configuration. -struct AccessMethodActionSheetConfiguration: Equatable { - /// The sheet presentation context. - var context: AccessMethodActionSheetContext = .addNew - - /// The sheet content configuration. - var contentConfiguration = AccessMethodActionSheetContentConfiguration() -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetContainerView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetContainerView.swift deleted file mode 100644 index 377e204f22b2..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetContainerView.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// AccessMethodActionSheetContainerView.swift -// MullvadVPN -// -// Created by pronebird on 15/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -/// The view implementing a vertical stack layout with the testing progress UI (content view) at the top and action buttons below. -class AccessMethodActionSheetContainerView: UIView { - /// Sheet delegate. - weak var delegate: AccessMethodActionSheetDelegate? - - /// Active configuration. - var configuration = AccessMethodActionSheetConfiguration() { - didSet { - contentView.configuration = configuration.contentConfiguration - updateView() - } - } - - private lazy var stackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [contentView, addButton, cancelButton]) - stackView.axis = .vertical - stackView.spacing = UIMetrics.padding16 - return stackView - }() - - private let contentView = AccessMethodActionSheetContentView() - private let cancelButton: AppButton = { - let button = AppButton(style: .tableInsetGroupedDefault) - button.setTitle( - NSLocalizedString("SHEET_CANCEL_BUTTON", tableName: "APIAccess", value: "Cancel", comment: ""), - for: .normal - ) - return button - }() - - private let addButton: AppButton = { - let button = AppButton(style: .tableInsetGroupedDanger) - button.setTitle( - NSLocalizedString("SHEET_ADD_ANYWAY_BUTTON", tableName: "APIAccess", value: "Add anyway", comment: ""), - for: .normal - ) - button.isHidden = true - return button - }() - - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - addActions() - updateView() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - directionalLayoutMargins = UIMetrics.contentLayoutMargins - - addConstrainedSubviews([stackView]) { - stackView.pinEdgesToSuperviewMargins() - } - } - - private func addActions() { - let cancelAction = UIAction { [weak self] _ in - self?.sendSheetDidCancel() - } - - let addAction = UIAction { [weak self] _ in - self?.sendSheetDidAdd() - } - - cancelButton.addAction(cancelAction, for: .touchUpInside) - addButton.addAction(addAction, for: .touchUpInside) - } - - private func updateView() { - let status = configuration.contentConfiguration.status - - switch configuration.context { - case .addNew: - addButton.isHidden = status != .unreachable - cancelButton.isEnabled = status != .reachable - - case .proxyConfiguration: - addButton.isHidden = true - cancelButton.isEnabled = status == .testing - } - } - - private func sendSheetDidAdd() { - delegate?.sheetDidAdd(self) - } - - private func sendSheetDidCancel() { - delegate?.sheetDidCancel(self) - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetDelegate.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetDelegate.swift deleted file mode 100644 index 0341cd80e01f..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Content view/AccessMethodActionSheetDelegate.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// AccessMethodActionSheetDelegate.swift -// MullvadVPN -// -// Created by pronebird on 22/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// Sheet container view delegate. -protocol AccessMethodActionSheetDelegate: AnyObject { - /// User tapped the cancel button. - func sheetDidCancel(_ sheet: AccessMethodActionSheetContainerView) - - /// User tapped the add button. - func sheetDidAdd(_ sheet: AccessMethodActionSheetContainerView) -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentation.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentation.swift deleted file mode 100644 index 3d801a14f3ef..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentation.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// AddAccessMethodActionSheetPresentation.swift -// MullvadVPN -// -// Created by pronebird on 16/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -/// Class responsible for presentation of access method sheet within the hosting view. -class AccessMethodActionSheetPresentation { - /// The view managed by the sheet presentation. - private let presentationView = AccessMethodActionSheetPresentationView(frame: CGRect( - x: 0, - y: 0, - width: 320, - height: 240 - )) - - /// Indicates whether the sheet is being presented. - private(set) var isPresenting = false - - /// Layout frame of a sheet view. - var sheetLayoutFrame: CGRect { - presentationView.sheetLayoutFrame - } - - /// Delegate. - weak var delegate: AccessMethodActionSheetPresentationDelegate? - - /// Presentation configuration. - var configuration: AccessMethodActionSheetPresentationConfiguration { - get { - presentationView.configuration - } - set { - presentationView.configuration = newValue - } - } - - init() { - presentationView.sheetDelegate = self - presentationView.alpha = 0 - } - - /// Present the sheet within the hosting view. - /// - /// - Parameters: - /// - parent: the hosting view. - /// - animated: whether to animate the transition. - func show(in parent: UIView, animated: Bool = true) { - guard !isPresenting || presentationView.superview != parent else { return } - - isPresenting = true - embed(into: parent) - - UIViewPropertyAnimator.runningPropertyAnimator( - withDuration: animated ? UIMetrics.AccessMethodActionSheetTransition.duration.timeInterval : 0, - delay: 0, - options: UIMetrics.AccessMethodActionSheetTransition.animationOptions - ) { - self.presentationView.alpha = 1 - } - } - - /// Hide the sheet from the hosting view. - /// - /// The sheet is removed from the hosting view after animation. - /// - /// - Parameter animated: whether to animate the transition. - func hide(animated: Bool = true) { - guard isPresenting else { return } - - isPresenting = false - - UIViewPropertyAnimator.runningPropertyAnimator( - withDuration: animated ? UIMetrics.AccessMethodActionSheetTransition.duration.timeInterval : 0, - delay: 0, - options: UIMetrics.AccessMethodActionSheetTransition.animationOptions - ) { - self.presentationView.alpha = 0 - } completion: { position in - guard position == .end else { return } - - self.presentationView.removeFromSuperview() - } - } - - /// Embed the container into the sheet container view into the hosting view. - /// - /// - Parameter parent: the hosting view. - private func embed(into parent: UIView) { - guard presentationView.superview != parent else { return } - - presentationView.removeFromSuperview() - parent.addConstrainedSubviews([presentationView]) { - presentationView.pinEdgesToSuperview() - } - } -} - -extension AccessMethodActionSheetPresentation: AccessMethodActionSheetDelegate { - func sheetDidAdd(_ sheet: AccessMethodActionSheetContainerView) { - delegate?.sheetDidAdd(sheetPresentation: self) - } - - func sheetDidCancel(_ sheet: AccessMethodActionSheetContainerView) { - delegate?.sheetDidCancel(sheetPresentation: self) - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentationConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentationConfiguration.swift deleted file mode 100644 index cc81c831d744..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentationConfiguration.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AccessMethodActionSheetPresentationConfiguration.swift -// MullvadVPN -// -// Created by pronebird on 28/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// Sheet presentation configuration. -struct AccessMethodActionSheetPresentationConfiguration: Equatable { - /// Whether presentation dims background. - /// When set to `false` the background is made transparent and all touches are passed through enabling interaction with the underlying view. - var dimsBackground = true - - /// Whether presentation blurs the background behind the sheet pinned at the bottom. - var blursSheetBackground = true - - /// Sheet configuration. - var sheetConfiguration = AccessMethodActionSheetConfiguration() -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentationDelegate.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentationDelegate.swift deleted file mode 100644 index b32b47ee5e76..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentationDelegate.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// AccessMethodActionSheetPresentationDelegate.swift -// MullvadVPN -// -// Created by pronebird on 22/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// Sheet presentation delegate. -protocol AccessMethodActionSheetPresentationDelegate: AnyObject { - /// User tapped the cancel button. - func sheetDidCancel(sheetPresentation: AccessMethodActionSheetPresentation) - - /// User tapped the add button. - func sheetDidAdd(sheetPresentation: AccessMethodActionSheetPresentation) -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentationView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentationView.swift deleted file mode 100644 index d561e5c52321..000000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Sheet/Presentation/AccessMethodActionSheetPresentationView.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// AccessMethodActionSheetPresentationView.swift -// MullvadVPN -// -// Created by pronebird on 28/11/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -/// The sheet presentation view implementing a layout similar to the one used by system action sheet. -class AccessMethodActionSheetPresentationView: UIView { - /// The dimming background view. - private let backgroundView: UIView = { - let backgroundView = UIView() - backgroundView.backgroundColor = .secondaryColor.withAlphaComponent(0.5) - return backgroundView - }() - - /// The blur view displayed behind the sheet. - private let sheetBlurBackgroundView: UIVisualEffectView = { - let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - blurView.directionalLayoutMargins = .zero - blurView.contentView.directionalLayoutMargins = .zero - return blurView - }() - - /// Sheet container view that contains action buttons and access method testing progress UI. - private let sheetView = AccessMethodActionSheetContainerView(frame: CGRect(x: 0, y: 0, width: 100, height: 44)) - - /// Layout frame of a sheet content view. - var sheetLayoutFrame: CGRect { - sheetView.convert(sheetView.bounds, to: self) - } - - /// Sheet delegate. - weak var sheetDelegate: AccessMethodActionSheetDelegate? { - get { - sheetView.delegate - } - set { - sheetView.delegate = newValue - } - } - - /// Presentation configuration. - var configuration = AccessMethodActionSheetPresentationConfiguration() { - didSet { - updateSubviews(previousConfiguration: oldValue, animated: window != nil) - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - - addBackgroundView() - updateSubviews(animated: false) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - if configuration.dimsBackground { - super.point(inside: point, with: event) - } else { - // Accept touches to the content view only when background view is hidden to enable user interaction with - // the view beneath. - sheetView.frame.contains(point) - } - } - - private func addBackgroundView() { - addConstrainedSubviews([backgroundView]) { - backgroundView.pinEdgesToSuperview() - } - } - - private func updateSubviews( - previousConfiguration: AccessMethodActionSheetPresentationConfiguration? = nil, - animated: Bool - ) { - if previousConfiguration?.blursSheetBackground != configuration.blursSheetBackground { - updateSheetBackground() - } - - if previousConfiguration?.dimsBackground != configuration.dimsBackground { - updateBackgroundView(animated: animated) - } - - sheetView.configuration = configuration.sheetConfiguration - } - - private func updateSheetBackground() { - sheetView.removeFromSuperview() - sheetBlurBackgroundView.removeFromSuperview() - - // Embed the sheet view into blur view when configured to blur the sheet's background. - if configuration.blursSheetBackground { - sheetBlurBackgroundView.contentView.addConstrainedSubviews([sheetView]) { - sheetView.pinEdgesToSuperviewMargins() - } - addConstrainedSubviews([sheetBlurBackgroundView]) { - sheetBlurBackgroundView.pinEdgesToSuperview(.all().excluding(.top)) - } - } else { - addConstrainedSubviews([sheetView]) { - sheetView.pinEdgesToSuperviewMargins(.all().excluding(.top)) - } - } - } - - private func updateBackgroundView(animated: Bool) { - UIViewPropertyAnimator.runningPropertyAnimator( - withDuration: animated ? UIMetrics.AccessMethodActionSheetTransition.duration.timeInterval : 0, - delay: 0, - options: UIMetrics.AccessMethodActionSheetTransition.animationOptions, - animations: { - self.backgroundView.alpha = self.configuration.dimsBackground ? 1 : 0 - } - ) - } -} diff --git a/ios/MullvadVPN/Extensions/UIBackgroundConfiguration+Extensions.swift b/ios/MullvadVPN/Extensions/UIBackgroundConfiguration+Extensions.swift index c799388434e1..e5a8339d1b83 100644 --- a/ios/MullvadVPN/Extensions/UIBackgroundConfiguration+Extensions.swift +++ b/ios/MullvadVPN/Extensions/UIBackgroundConfiguration+Extensions.swift @@ -11,7 +11,7 @@ import UIKit extension UIBackgroundConfiguration { /// Type of cell selection used in Mullvad UI. enum CellSelectionType { - /// Dimmed blue . + /// Dimmed blue. case dimmed /// Bright green. case green @@ -39,7 +39,10 @@ extension UIBackgroundConfiguration { /// - state: a cell state. /// - selectionType: a desired selecton type. /// - Returns: new background configuration. - func adapted(for state: UICellConfigurationState, selectionType: CellSelectionType) -> UIBackgroundConfiguration { + func adapted( + for state: UICellConfigurationState, + selectionType: CellSelectionType + ) -> UIBackgroundConfiguration { var config = self config.backgroundColor = state.mullvadCellBackgroundColor(selectionType: selectionType) return config @@ -63,6 +66,8 @@ extension UICellConfigurationState { case .dimmed: if isSelected || isHighlighted { UIColor.Cell.selectedAltBackgroundColor + } else if isDisabled { + UIColor.Cell.disabledBackgroundColor } else { UIColor.Cell.backgroundColor } @@ -70,6 +75,8 @@ extension UICellConfigurationState { case .green: if isSelected || isHighlighted { UIColor.Cell.selectedBackgroundColor + } else if isDisabled { + UIColor.Cell.disabledBackgroundColor } else { UIColor.Cell.backgroundColor } diff --git a/ios/MullvadVPN/Extensions/UIListContentConfiguration+Extensions.swift b/ios/MullvadVPN/Extensions/UIListContentConfiguration+Extensions.swift index ad3da9a9ca23..242abbbfbdfe 100644 --- a/ios/MullvadVPN/Extensions/UIListContentConfiguration+Extensions.swift +++ b/ios/MullvadVPN/Extensions/UIListContentConfiguration+Extensions.swift @@ -10,51 +10,70 @@ import UIKit extension UIListContentConfiguration { /// Returns cell configured with default text attribute used in Mullvad UI. - static func mullvadCell(tableStyle: UITableView.Style) -> UIListContentConfiguration { + static func mullvadCell(tableStyle: UITableView.Style, isEnabled: Bool = true) -> UIListContentConfiguration { var configuration = cell() - configuration.textProperties.font = UIFont.systemFont(ofSize: 17) - configuration.textProperties.color = UIColor.Cell.titleTextColor - configuration.directionalLayoutMargins = tableStyle.directionalLayoutMarginsForCell + configuration.textProperties.font = .systemFont(ofSize: 17) + configuration.textProperties.color = .Cell.titleTextColor.withAlphaComponent(isEnabled ? 1 : 0.8) + configuration.axesPreservingSuperviewLayoutMargins = .vertical + + applyMargins(to: &configuration, tableStyle: tableStyle) + return configuration } /// Returns value cell configured with default text attribute used in Mullvad UI. - static func mullvadValueCell(tableStyle: UITableView.Style) -> UIListContentConfiguration { + static func mullvadValueCell(tableStyle: UITableView.Style, isEnabled: Bool = true) -> UIListContentConfiguration { var configuration = valueCell() - configuration.textProperties.font = UIFont.systemFont(ofSize: 17) - configuration.textProperties.color = UIColor.Cell.titleTextColor - configuration.secondaryTextProperties.color = UIColor.Cell.detailTextColor - configuration.secondaryTextProperties.font = UIFont.systemFont(ofSize: 17) - configuration.directionalLayoutMargins = tableStyle.directionalLayoutMarginsForCell + configuration.textProperties.font = .systemFont(ofSize: 17) + configuration.textProperties.color = .Cell.titleTextColor.withAlphaComponent(isEnabled ? 1 : 0.8) + configuration.secondaryTextProperties.color = .Cell.detailTextColor.withAlphaComponent(isEnabled ? 1 : 0.8) + configuration.secondaryTextProperties.font = .systemFont(ofSize: 17) + + applyMargins(to: &configuration, tableStyle: tableStyle) + return configuration } /// Returns grouped header configured with default text attribute used in Mullvad UI. - static func mullvadGroupedHeader() -> UIListContentConfiguration { + static func mullvadGroupedHeader(tableStyle: UITableView.Style) -> UIListContentConfiguration { var configuration = groupedHeader() - configuration.textProperties.color = UIColor.TableSection.headerTextColor - configuration.textProperties.font = UIFont.systemFont(ofSize: 17) + configuration.textProperties.color = .TableSection.headerTextColor + configuration.textProperties.font = .systemFont(ofSize: 13) + + applyMargins(to: &configuration, tableStyle: tableStyle) + return configuration } /// Returns grouped footer configured with default text attribute used in Mullvad UI. - static func mullvadGroupedFooter() -> UIListContentConfiguration { + static func mullvadGroupedFooter(tableStyle: UITableView.Style) -> UIListContentConfiguration { var configuration = groupedFooter() - configuration.textProperties.color = UIColor.TableSection.footerTextColor - configuration.textProperties.font = UIFont.systemFont(ofSize: 14) + configuration.textProperties.color = .TableSection.footerTextColor + configuration.textProperties.font = .systemFont(ofSize: 13) + + applyMargins(to: &configuration, tableStyle: tableStyle) + return configuration } + + private static func applyMargins( + to configuration: inout UIListContentConfiguration, + tableStyle: UITableView.Style + ) { + configuration.axesPreservingSuperviewLayoutMargins = .vertical + configuration.directionalLayoutMargins = tableStyle.directionalLayoutMarginsForCell + } } extension UITableView.Style { var directionalLayoutMarginsForCell: NSDirectionalEdgeInsets { switch self { case .plain, .grouped: - UIMetrics.SettingsCell.layoutMargins + UIMetrics.SettingsCell.apiAccessLayoutMargins case .insetGrouped: - UIMetrics.SettingsCell.insetLayoutMargins + UIMetrics.SettingsCell.apiAccessInsetLayoutMargins @unknown default: - UIMetrics.SettingsCell.layoutMargins + UIMetrics.SettingsCell.apiAccessLayoutMargins } } } diff --git a/ios/MullvadVPN/Extensions/UITableViewCell+Disable.swift b/ios/MullvadVPN/Extensions/UITableViewCell+Disable.swift new file mode 100644 index 000000000000..6b739e73005c --- /dev/null +++ b/ios/MullvadVPN/Extensions/UITableViewCell+Disable.swift @@ -0,0 +1,16 @@ +// +// UITableViewCell+Disable.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-01-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +extension UITableViewCell { + func setDisabled(_ disabled: Bool) { + isUserInteractionEnabled = !disabled + contentView.alpha = disabled ? 0.8 : 1 + } +} diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift index 33c2e69e926a..a04e1e1ae041 100644 --- a/ios/MullvadVPN/UI appearance/UIMetrics.swift +++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift @@ -56,7 +56,7 @@ enum UIMetrics { } enum SettingsRedeemVoucher { - static let cornerRadius = 8.0 + static let cornerRadius: CGFloat = 8 static let preferredContentSize = CGSize(width: 280, height: 260) static let contentLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16) static let successfulRedeemMargins = NSDirectionalEdgeInsets(top: 16, leading: 8, bottom: 16, trailing: 8) @@ -67,7 +67,7 @@ enum UIMetrics { } enum Button { - static let barButtonSize: CGFloat = 44.0 + static let barButtonSize: CGFloat = 44 } enum SettingsCell { @@ -80,6 +80,12 @@ enum UIMetrics { /// Cell layout margins used in table views that use inset style. static let insetLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24) + + static let apiAccessLayoutMargins = NSDirectionalEdgeInsets(top: 20, leading: 16, bottom: 20, trailing: 16) + static let apiAccessInsetLayoutMargins = NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) + static let apiAccessCellHeight: CGFloat = 44 + static let apiAccessSwitchCellTrailingMargin: CGFloat = apiAccessInsetLayoutMargins.trailing - 4 + static let apiAccessPickerListContentInsetTop: CGFloat = 16 } enum InAppBannerNotification { diff --git a/ios/MullvadVPN/Views/AppButton.swift b/ios/MullvadVPN/Views/AppButton.swift index 245a0f5f9a39..c2581d5c7180 100644 --- a/ios/MullvadVPN/Views/AppButton.swift +++ b/ios/MullvadVPN/Views/AppButton.swift @@ -71,11 +71,11 @@ class AppButton: CustomButton { case .translucentDangerSplitRight: UIImage(resource: .translucentDangerSplitRightButton).imageFlippedForRightToLeftLayoutDirection() case .tableInsetGroupedDefault: - DynamicAssets.shared.tableInsetGroupedDefaultBackground + UIImage(resource: .defaultButton) case .tableInsetGroupedSuccess: - DynamicAssets.shared.tableInsetGroupedSuccessBackground + UIImage(resource: .successButton) case .tableInsetGroupedDanger: - DynamicAssets.shared.tableInsetGroupedDangerBackground + UIImage(resource: .dangerButton) } } } @@ -169,37 +169,3 @@ class AppButton: CustomButton { } } } - -private extension AppButton { - class DynamicAssets { - static let shared = DynamicAssets() - - private init() {} - - /// Default cell corner radius in inset grouped table view - private let tableViewCellCornerRadius: CGFloat = 10 - - lazy var tableInsetGroupedDefaultBackground: UIImage = { - roundedRectImage(fillColor: .primaryColor) - }() - - lazy var tableInsetGroupedSuccessBackground: UIImage = { - roundedRectImage(fillColor: .successColor) - }() - - lazy var tableInsetGroupedDangerBackground: UIImage = { - roundedRectImage(fillColor: .dangerColor) - }() - - private func roundedRectImage(fillColor: UIColor) -> UIImage { - let cornerRadius = tableViewCellCornerRadius - let bounds = CGRect(x: 0, y: 0, width: 44, height: 44) - let image = UIGraphicsImageRenderer(bounds: bounds).image { _ in - fillColor.setFill() - UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).fill() - } - let caps = UIEdgeInsets(top: cornerRadius, left: cornerRadius, bottom: cornerRadius, right: cornerRadius) - return image.resizableImage(withCapInsets: caps) - } - } -} diff --git a/ios/MullvadVPNTests/APIAccessMethodsTests.swift b/ios/MullvadVPNTests/APIAccessMethodsTests.swift index 2163c900c6d4..a0a7072233d0 100644 --- a/ios/MullvadVPNTests/APIAccessMethodsTests.swift +++ b/ios/MullvadVPNTests/APIAccessMethodsTests.swift @@ -46,7 +46,7 @@ final class APIAccessMethodsTests: XCTestCase { let uuid = UUID() let methodToStore = socks5AccessMethod(with: uuid) - AccessMethodRepository.shared.add(methodToStore) + AccessMethodRepository.shared.save(methodToStore) let storedMethod = AccessMethodRepository.shared.fetch(by: uuid) XCTAssertEqual(methodToStore.id, storedMethod?.id) @@ -56,7 +56,7 @@ final class APIAccessMethodsTests: XCTestCase { let uuid = UUID() let methodToStore = shadowsocksAccessMethod(with: uuid) - AccessMethodRepository.shared.add(methodToStore) + AccessMethodRepository.shared.save(methodToStore) let storedMethod = AccessMethodRepository.shared.fetch(by: uuid) XCTAssertEqual(methodToStore.id, storedMethod?.id) @@ -65,8 +65,8 @@ final class APIAccessMethodsTests: XCTestCase { func testAddingDuplicateAccessMethodDoesNothing() throws { let methodToStore = socks5AccessMethod(with: UUID()) - AccessMethodRepository.shared.add(methodToStore) - AccessMethodRepository.shared.add(methodToStore) + AccessMethodRepository.shared.save(methodToStore) + AccessMethodRepository.shared.save(methodToStore) let storedMethods = AccessMethodRepository.shared.fetchAll() // Account for .direct and .bridges that are always added by default. @@ -77,12 +77,12 @@ final class APIAccessMethodsTests: XCTestCase { let uuid = UUID() var methodToStore = socks5AccessMethod(with: uuid) - AccessMethodRepository.shared.add(methodToStore) + AccessMethodRepository.shared.save(methodToStore) let newName = "Renamed method" methodToStore.name = newName - AccessMethodRepository.shared.update(methodToStore) + AccessMethodRepository.shared.save(methodToStore) let storedMethod = AccessMethodRepository.shared.fetch(by: uuid) @@ -93,7 +93,7 @@ final class APIAccessMethodsTests: XCTestCase { let uuid = UUID() let methodToStore = socks5AccessMethod(with: uuid) - AccessMethodRepository.shared.add(methodToStore) + AccessMethodRepository.shared.save(methodToStore) AccessMethodRepository.shared.delete(id: uuid) let storedMethod = AccessMethodRepository.shared.fetch(by: uuid)