From 118080b93845bcd6d6d5fcbc87d1d2b7c12826c6 Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Wed, 4 Dec 2024 10:18:22 +0100 Subject: [PATCH] Add toggle in connection view --- .../SelectedRelaysStub+Stubs.swift | 36 +++ .../EphemeralPeerNegotiator.swift | 3 - .../MullvadPostQuantum+Stubs.swift | 3 - ios/MullvadVPN.xcodeproj/project.pbxproj | 24 +- .../Classes/AccessbilityIdentifier.swift | 5 +- .../Coordinators/TunnelCoordinator.swift | 9 + .../Extensions/String+Helpers.swift | 6 +- .../Extensions/View+TapAreaSize.swift | 4 +- .../IconReload.imageset/Contents.json | 2 +- .../IconReload.imageset/IconReload.pdf | Bin 1236 -> 0 bytes .../IconReload.imageset/icon-reload.svg | 10 + ios/MullvadVPN/UI appearance/UIMetrics.swift | 5 + .../FeatureIndicators/ChipFeatures.swift | 88 ------- .../ChipView/ChipContainerView.swift | 68 +++--- .../ChipView/ChipFeature.swift | 123 ++++++++++ .../ChipView/ChipModel.swift | 2 +- .../FeatureIndicators/ChipView/ChipView.swift | 10 +- .../ChipView/ChipViewModelProtocol.swift | 59 +++-- .../FeatureIndicators/ConnectionView.swift | 219 ++++++++++++++---- .../ConnectionViewPreview.swift | 81 +++++++ .../ConnectionViewViewModel.swift | 139 ++++++++--- .../FI_TunnelViewController.swift | 12 +- .../FeatureIndicatorsView.swift | 24 +- .../FeatureIndicatorsViewModel.swift | 6 +- .../Tunnel/TunnelControlView.swift | 4 +- ios/MullvadVPN/Views/MainButton.swift | 3 +- ios/MullvadVPN/Views/MainButtonStyle.swift | 24 +- ios/MullvadVPN/Views/SplitMainButton.swift | 14 +- .../Pages/TunnelControlPage.swift | 4 +- .../Actor/PacketTunnelActor.swift | 2 +- ios/convert-assets.rb | 1 - 31 files changed, 711 insertions(+), 279 deletions(-) create mode 100644 ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift delete mode 100644 ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf create mode 100644 ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/icon-reload.svg delete mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift create mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift create mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift diff --git a/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift b/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift new file mode 100644 index 000000000000..adc749223000 --- /dev/null +++ b/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift @@ -0,0 +1,36 @@ +// +// SelectedRelaysStub+Stubs.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-12-18. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadREST +import MullvadTypes +import Network + +public struct SelectedRelaysStub { + public static let selectedRelays = SelectedRelays( + entry: nil, + exit: SelectedRelay( + endpoint: MullvadEndpoint( + ipv4Relay: IPv4Endpoint(ip: .loopback, port: 42), + ipv6Relay: IPv6Endpoint(ip: .loopback, port: 42), + ipv4Gateway: IPv4Address.loopback, + ipv6Gateway: IPv6Address.loopback, + publicKey: Data() + ), + hostname: "se-got-wg-001", + location: Location( + country: "Sweden", + countryCode: "se", + city: "Gothenburg", + cityCode: "got", + latitude: 42, + longitude: 42 + ) + ), + retryAttempt: 0 + ) +} diff --git a/ios/MullvadRustRuntime/EphemeralPeerNegotiator.swift b/ios/MullvadRustRuntime/EphemeralPeerNegotiator.swift index 8346b2686d45..85503c8bf468 100644 --- a/ios/MullvadRustRuntime/EphemeralPeerNegotiator.swift +++ b/ios/MullvadRustRuntime/EphemeralPeerNegotiator.swift @@ -11,7 +11,6 @@ import MullvadTypes import NetworkExtension import WireGuardKitTypes -// swiftlint:disable function_parameter_count public protocol EphemeralPeerNegotiating { func startNegotiation( devicePublicKey: PublicKey, @@ -70,5 +69,3 @@ public class EphemeralPeerNegotiator: EphemeralPeerNegotiating { drop_ephemeral_peer_exchange_token(cancelToken) } } - -// swiftlint:enable function_parameter_count diff --git a/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift b/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift index 52e4742c2126..ff7a3f74a4e3 100644 --- a/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift +++ b/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift @@ -12,7 +12,6 @@ import NetworkExtension @testable import PacketTunnelCore @testable import WireGuardKitTypes -// swiftlint:disable function_parameter_count class NWTCPConnectionStub: NWTCPConnection { var _isViable = false override var isViable: Bool { @@ -104,5 +103,3 @@ class SuccessfulNegotiatorStub: EphemeralPeerNegotiating { onCancelKeyNegotiation?() } } - -// swiftlint:enable function_parameter_count diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 991c24673d2a..e0b6ce6dd4f8 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -653,6 +653,8 @@ 7AF10EB42ADE85BC00C090B9 /* RelayFilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF10EB32ADE85BC00C090B9 /* RelayFilterCoordinator.swift */; }; 7AF36A9A2CA2964200E1D497 /* AnyIPAddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF36A992CA2964000E1D497 /* AnyIPAddressTests.swift */; }; 7AF6E5F02A95051E00F2679D /* RouterBlockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */; }; + 7AF84F462D12C5B000C72690 /* SelectedRelaysStub+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */; }; + 7AF84F482D12C9D400C72690 /* ConnectionViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */; }; 7AF9BE882A30C62100DBFEDB /* SelectableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1A264A2A29D65E00B978AA /* SelectableSettingsCell.swift */; }; 7AF9BE8C2A321D1F00DBFEDB /* RelayFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE8A2A321BEF00DBFEDB /* RelayFilter.swift */; }; 7AF9BE8E2A331C7B00DBFEDB /* RelayFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE8D2A331C7B00DBFEDB /* RelayFilterViewModel.swift */; }; @@ -1003,7 +1005,7 @@ F0B0E6972AFE6E7E001DC66B /* XCTest+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */; }; F0B495762D02025200CFEC2A /* ChipContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495752D02025200CFEC2A /* ChipContainerView.swift */; }; F0B495782D02038B00CFEC2A /* ChipViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */; }; - F0B4957A2D02F49200CFEC2A /* ChipFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */; }; + F0B4957A2D02F49200CFEC2A /* ChipFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495792D02F41F00CFEC2A /* ChipFeature.swift */; }; F0B4957C2D03154200CFEC2A /* FeatureIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */; }; F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */; }; F0B894F12BF751E300817A42 /* RelayWithDistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894F02BF751E300817A42 /* RelayWithDistance.swift */; }; @@ -2022,6 +2024,8 @@ 7AF10EB32ADE85BC00C090B9 /* RelayFilterCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayFilterCoordinator.swift; sourceTree = ""; }; 7AF36A992CA2964000E1D497 /* AnyIPAddressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPAddressTests.swift; sourceTree = ""; }; 7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterBlockDelegate.swift; sourceTree = ""; }; + 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SelectedRelaysStub+Stubs.swift"; sourceTree = ""; }; + 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionViewPreview.swift; sourceTree = ""; }; 7AF9BE8A2A321BEF00DBFEDB /* RelayFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilter.swift; sourceTree = ""; }; 7AF9BE8D2A331C7B00DBFEDB /* RelayFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterViewModel.swift; sourceTree = ""; }; 7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Sorting.swift"; sourceTree = ""; }; @@ -2249,7 +2253,7 @@ F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Async.swift"; sourceTree = ""; }; F0B495752D02025200CFEC2A /* ChipContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipContainerView.swift; sourceTree = ""; }; F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewModelProtocol.swift; sourceTree = ""; }; - F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFeatures.swift; sourceTree = ""; }; + F0B495792D02F41F00CFEC2A /* ChipFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFeature.swift; sourceTree = ""; }; F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIndicatorsView.swift; sourceTree = ""; }; F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithLocation.swift; sourceTree = ""; }; F0B894F02BF751E300817A42 /* RelayWithDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithDistance.swift; sourceTree = ""; }; @@ -3638,7 +3642,6 @@ A9D9A4C12C36D53C004088DD /* MullvadRustRuntimeTests */, 58CE5E61224146200008646E /* Products */, 584F991F2902CBDD001F858D /* Frameworks */, - 7A0EAE982D01B29E00D3EB8B /* Recovered References */, ); sourceTree = ""; }; @@ -3939,13 +3942,6 @@ path = Edit; sourceTree = ""; }; - 7A0EAE982D01B29E00D3EB8B /* Recovered References */ = { - isa = PBXGroup; - children = ( - ); - name = "Recovered References"; - sourceTree = ""; - }; 7A2960F72A964A3500389B82 /* Alert */ = { isa = PBXGroup; children = ( @@ -4082,8 +4078,8 @@ children = ( F0ADF1CF2D01B50B00299F09 /* ChipView */, 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */, - F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */, 7AA130982CFF365A00640DF9 /* ConnectionView.swift */, + 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */, 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */, F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */, F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */, @@ -4396,6 +4392,7 @@ F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */, 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */, A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */, + 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */, ); path = MullvadREST; sourceTree = ""; @@ -4413,6 +4410,7 @@ isa = PBXGroup; children = ( F0B495752D02025200CFEC2A /* ChipContainerView.swift */, + F0B495792D02F41F00CFEC2A /* ChipFeature.swift */, F0ADF1D02D01B55C00299F09 /* ChipModel.swift */, F0ADF1D42D01DCFD00299F09 /* ChipView.swift */, F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */, @@ -5935,6 +5933,7 @@ 7A27E3CB2CAE861D0088BCFF /* SettingsViewModel.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, 7A9F29392CABFAFC005F2089 /* InfoHeaderView.swift in Sources */, + 7AF84F482D12C9D400C72690 /* ConnectionViewPreview.swift in Sources */, 58DFF7D22B0256A300F864E0 /* MarkdownStylingOptions.swift in Sources */, 5867770E29096984006F721F /* OutOfTimeInteractor.swift in Sources */, F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */, @@ -6130,7 +6129,7 @@ 588D7EDE2AF3A585005DF40A /* ListAccessMethodItem.swift in Sources */, 5827B0B02B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift in Sources */, 588D7EE02AF3A595005DF40A /* ListAccessMethodInteractor.swift in Sources */, - F0B4957A2D02F49200CFEC2A /* ChipFeatures.swift in Sources */, + F0B4957A2D02F49200CFEC2A /* ChipFeature.swift in Sources */, 58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */, 7A8A18FD2CE4BE8D000BCB5B /* CustomToggleStyle.swift in Sources */, 58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */, @@ -6514,6 +6513,7 @@ F0ACE3332BE516F1006D5333 /* RESTRequestExecutor+Stubs.swift in Sources */, F0ACE32D2BE4E784006D5333 /* AccountMock.swift in Sources */, 7A52F96A2C1735AE00B133B9 /* RelaySelectorStub.swift in Sources */, + 7AF84F462D12C5B000C72690 /* SelectedRelaysStub+Stubs.swift in Sources */, F03A69F72C2AD2D6000E2E7E /* TimeInterval+Timeout.swift in Sources */, F0ACE32F2BE4EA8B006D5333 /* MockProxyFactory.swift in Sources */, ); diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 5aa1e0c86823..4f93def48b96 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -40,7 +40,7 @@ public enum AccessibilityIdentifier: Equatable { case purchaseButton case redeemVoucherButton case restorePurchasesButton - case secureConnectionButton + case connectButton case selectLocationButton case closeSelectLocationButton case settingsButton @@ -132,7 +132,7 @@ public enum AccessibilityIdentifier: Equatable { case selectLocationTableView case settingsTableView case vpnSettingsTableView - case tunnelControlView + case connectionView case problemReportView case problemReportSubmittedView case revokedDeviceView @@ -156,6 +156,7 @@ public enum AccessibilityIdentifier: Equatable { case logOutSpinnerAlertView case connectionPanelInAddressRow case connectionPanelOutAddressRow + case connectionPanelOutIpv6AddressRow case customSwitch case customWireGuardPortTextField case dnsContentBlockersHeaderView diff --git a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift index 7a8145ddca51..0c55f7e1af46 100644 --- a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift @@ -12,7 +12,12 @@ import UIKit class TunnelCoordinator: Coordinator, Presenting { private let tunnelManager: TunnelManager + + #if DEBUG + private let controller: FI_TunnelViewController + #else private let controller: TunnelViewController + #endif private var tunnelObserver: TunnelObserver? @@ -39,7 +44,11 @@ class TunnelCoordinator: Coordinator, Presenting { ipOverrideRepository: ipOverrideRepository ) + #if DEBUG + controller = FI_TunnelViewController(interactor: interactor) + #else controller = TunnelViewController(interactor: interactor) + #endif super.init() diff --git a/ios/MullvadVPN/Extensions/String+Helpers.swift b/ios/MullvadVPN/Extensions/String+Helpers.swift index a3112819405b..512adaa6f766 100644 --- a/ios/MullvadVPN/Extensions/String+Helpers.swift +++ b/ios/MullvadVPN/Extensions/String+Helpers.swift @@ -6,7 +6,6 @@ // Copyright © 2020 Mullvad VPN AB. All rights reserved. // -import Foundation import UIKit extension String { @@ -19,4 +18,9 @@ extension String { return (0 ..< resultCount) .map { dropFirst($0 * length).prefix(length) } } + + func width(using font: UIFont) -> CGFloat { + let fontAttributes = [NSAttributedString.Key.font: font] + return self.size(withAttributes: fontAttributes).width + } } diff --git a/ios/MullvadVPN/Extensions/View+TapAreaSize.swift b/ios/MullvadVPN/Extensions/View+TapAreaSize.swift index 1e4ed64d3790..bf81cd57be35 100644 --- a/ios/MullvadVPN/Extensions/View+TapAreaSize.swift +++ b/ios/MullvadVPN/Extensions/View+TapAreaSize.swift @@ -17,8 +17,8 @@ extension View { } private struct TappablePadding: ViewModifier { - @State var actualViewSize: CGSize = .zero - let tappableViewSize = UIMetrics.Button.minimumTappableAreaSize + @State private var actualViewSize: CGSize = .zero + private let tappableViewSize = UIMetrics.Button.minimumTappableAreaSize func body(content: Content) -> some View { content diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json index ff6e72343274..fc394e1bfbbb 100644 --- a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "IconReload.pdf", + "filename" : "icon-reload.svg", "idiom" : "universal" } ], diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf deleted file mode 100644 index d58fb05aa5f85791ec24399c337106dc6b8a5e48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1236 zcmY!laB6B^|0=W56?w_G&GL1#bH=>pDYs;?ec27I4i zaqG48jM;aBH40uZF6?~o`GGrdS8d;V!~XZb=gx~)-}|4Z_o$qk>->~ebGnSXRU$RR zxo2*kH933JLNC3zrzOhq$ATx<6s?ylQTI=&n#dNeakOO9QNh~2iFfoo?zC-DN}ON9 zmc5|v_Q8dlPGny?CVk`alHAvQe1EUpp4KTV^|CU;H{0B4QsFNtk8>wA3na7B-!WbD z;F|O@r_<@~ouCj_VV#UiuV)mCy+~ac@q2;SwVC`ARPO{l)PJ|6QdzDq=Ed3d+>xh( zqHSzUt{rRCc$;$3F3;FzL!s$xUqy*8N3Vw6x8261x!k;YSCn`gYwz*I_IF(TM=}gf zY?Ujp-^@L^eg$h?LG>+*iIYwoF!*@qFxR%s8JaAIZ}=)qS(lwssQ9F@d{Jv|BGcOI zS~;#_EH@NOn^?NH6h8DkDY^W_{YQ6SJiafruXp-K?zQXN9l28TQlLQ&WPu_J6hj~y z7)Qot1|WG5&lnz+;K*~WC~*%i0Y;pHjSWZu2=o&TfE)#VXGaA?1^whiAPJ04pahTv zBB(h=aB~#&Ln;eW74!oV(^C~x6%4`V1|$}x=9K`o!cvKEYD#9JQ+|a)G*HMu!PL+Q zjAOy7o%8cbfXa&%KnV?!s@#k6OAC-31+fDx3Ug;L(B`7Vyy60oUCEU&VNZpUqS90t z&l#x7CABOwIW@@L2_y*di+)gQaeir0a%!;xC`5vgdrx~3-BB4=RcDap*wa{)z-hLM4hfq}V^rJ0URipnVK4#Dx@jE#LUfr5ds1Vd2lgP z6QKXl#7xXF)fpH96Ck=eQ((xUiCGv~Vu)Fq03!vauB0e2GbgnOT=)cMR;2=+42p`N o{QMFHkjEkE#WOE0UjY>I;HWGvNh~S>`_R%7Scs{*`nz!f09k*DVgLXD diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/icon-reload.svg b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/icon-reload.svg new file mode 100644 index 000000000000..6d443ac8b4c0 --- /dev/null +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/icon-reload.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift index 1d0f98e8f06c..8eeb56c1598f 100644 --- a/ios/MullvadVPN/UI appearance/UIMetrics.swift +++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift @@ -138,6 +138,11 @@ enum UIMetrics { enum MainButton { static let cornerRadius: CGFloat = 4 } + + enum FeatureIndicators { + static let chipViewHorisontalPadding: CGFloat = 8 + static let chipViewTrailingMargin: CGFloat = 6 + } } extension UIMetrics { diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift deleted file mode 100644 index c005b3f080ed..000000000000 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// ChipFeatures.swift -// MullvadVPN -// -// Created by Mojgan on 2024-12-06. -// Copyright © 2024 Mullvad VPN AB. All rights reserved. -// -import Foundation -import MullvadSettings -import SwiftUI - -protocol ChipFeature { - var isEnabled: Bool { get } - var name: LocalizedStringKey { get } -} - -struct DaitaFeature: ChipFeature { - let settings: LatestTunnelSettings - - var isEnabled: Bool { - settings.daita.daitaState.isEnabled - } - - var name: LocalizedStringKey { - LocalizedStringKey("DAITA") - } -} - -struct QuantumResistanceFeature: ChipFeature { - let settings: LatestTunnelSettings - var isEnabled: Bool { - settings.tunnelQuantumResistance.isEnabled - } - - var name: LocalizedStringKey { - LocalizedStringKey("Quantum resistance") - } -} - -struct MultihopFeature: ChipFeature { - let settings: LatestTunnelSettings - var isEnabled: Bool { - settings.tunnelMultihopState.isEnabled - } - - var name: LocalizedStringKey { - LocalizedStringKey("Multihop") - } -} - -struct ObfuscationFeature: ChipFeature { - let settings: LatestTunnelSettings - - var isEnabled: Bool { - settings.wireGuardObfuscation.state.isEnabled - } - - var name: LocalizedStringKey { - LocalizedStringKey("Obfuscation") - } -} - -struct DNSFeature: ChipFeature { - let settings: LatestTunnelSettings - - var isEnabled: Bool { - settings.dnsSettings.enableCustomDNS || !settings.dnsSettings.blockingOptions.isEmpty - } - - var name: LocalizedStringKey { - if !settings.dnsSettings.blockingOptions.isEmpty { - return LocalizedStringKey("DNS content blockers") - } - return LocalizedStringKey("Custom DNS") - } -} - -struct IPOverrideFeature: ChipFeature { - let overrides: [IPOverride] - - var isEnabled: Bool { - !overrides.isEmpty - } - - var name: LocalizedStringKey { - LocalizedStringKey("Server IP override") - } -} diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift index f06124ed4811..e64874ad22ad 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift @@ -10,46 +10,42 @@ import SwiftUI struct ChipContainerView: View where ViewModel: ChipViewModelProtocol { @ObservedObject var viewModel: ViewModel + @Binding var isExpanded: Bool - @State var chipHeight: CGFloat = 0 - @State var fullContainerHeight: CGFloat = 0 - @State var visibleContainerHeight: CGFloat = 0 + @State private var chipContainerHeight: CGFloat = .zero + private let verticalPadding: CGFloat = 6 var body: some View { GeometryReader { geo in let containerWidth = geo.size.width - let chipsOverflow = !viewModel.isExpanded && (fullContainerHeight > chipHeight) - let numberOfChips = chipsOverflow ? 2 : viewModel.chips.count + + let (chipsToAdd, showMoreButton) = if isExpanded { + (viewModel.chips, false) + } else { + viewModel.chipsToAdd(forContainerWidth: containerWidth) + } HStack { ZStack(alignment: .topLeading) { - createChipViews(chips: Array(viewModel.chips.prefix(numberOfChips)), containerWidth: containerWidth) + createChipViews(chips: chipsToAdd, containerWidth: containerWidth) } - .sizeOfView { visibleContainerHeight = $0.height } - if chipsOverflow { - Text(LocalizedStringKey("\(viewModel.chips.count - numberOfChips) more...")) + if showMoreButton { + Text(LocalizedStringKey("\(viewModel.chips.count - chipsToAdd.count) more...")) .font(.subheadline) .lineLimit(1) .foregroundStyle(UIColor.primaryTextColor.color) - .padding(.bottom, 12) + .onTapGesture { + isExpanded.toggle() + } } Spacer() } - .background(preRenderViewSize(containerWidth: containerWidth)) - }.frame(height: visibleContainerHeight) - } - - // Renders all chips on screen, in this case specifically to get their combined height. - // Used to determine if content would overflow if view was not expanded and should - // only be called from a background modifier. - private func preRenderViewSize(containerWidth: CGFloat) -> some View { - ZStack(alignment: .topLeading) { - createChipViews(chips: viewModel.chips, containerWidth: containerWidth) + .sizeOfView { chipContainerHeight = $0.height } } - .hidden() - .sizeOfView { fullContainerHeight = $0.height } + .frame(height: chipContainerHeight) + .padding(.vertical, -(verticalPadding - 1)) // Remove extra padding from chip views on top and bottom. } private func createChipViews(chips: [ChipModel], containerWidth: CGFloat) -> some View { @@ -58,14 +54,21 @@ struct ChipContainerView: View where ViewModel: ChipViewModelProtocol return ForEach(chips) { data in ChipView(item: data) - .padding(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 8)) + .padding( + EdgeInsets( + top: verticalPadding, + leading: 0, + bottom: verticalPadding, + trailing: UIMetrics.FeatureIndicators.chipViewTrailingMargin + ) + ) .alignmentGuide(.leading) { dimension in if abs(width - dimension.width) > containerWidth { width = 0 height -= dimension.height } let result = width - if data.id == chips.last!.id { + if data.id == chips.last?.id { width = 0 } else { width -= dimension.width @@ -74,22 +77,27 @@ struct ChipContainerView: View where ViewModel: ChipViewModelProtocol } .alignmentGuide(.top) { _ in let result = height - if data.id == chips.last!.id { + if data.id == chips.last?.id { height = 0 } return result } - .sizeOfView { chipHeight = $0.height } } } } #Preview("Normal") { - ChipContainerView(viewModel: MockFeatureIndicatorsViewModel()) - .background(UIColor.secondaryColor.color) + ChipContainerView( + viewModel: MockFeatureIndicatorsViewModel(), + isExpanded: .constant(false) + ) + .background(UIColor.secondaryColor.color) } #Preview("Expanded") { - ChipContainerView(viewModel: MockFeatureIndicatorsViewModel(isExpanded: true)) - .background(UIColor.secondaryColor.color) + ChipContainerView( + viewModel: MockFeatureIndicatorsViewModel(), + isExpanded: .constant(true) + ) + .background(UIColor.secondaryColor.color) } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift new file mode 100644 index 000000000000..ea1d130dbede --- /dev/null +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipFeature.swift @@ -0,0 +1,123 @@ +// +// ChipFeature.swift +// MullvadVPN +// +// Created by Mojgan on 2024-12-06. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// +import MullvadSettings +import SwiftUI + +protocol ChipFeature { + var isEnabled: Bool { get } + var name: String { get } +} + +struct DaitaFeature: ChipFeature { + let settings: LatestTunnelSettings + + var isEnabled: Bool { + settings.daita.daitaState.isEnabled + } + + var name: String { + NSLocalizedString( + "FEATURE_INDICATORS_CHIP_DAITA", + tableName: "FeatureIndicatorsChip", + value: "DAITA", + comment: "" + ) + } +} + +struct QuantumResistanceFeature: ChipFeature { + let settings: LatestTunnelSettings + var isEnabled: Bool { + settings.tunnelQuantumResistance.isEnabled + } + + var name: String { + NSLocalizedString( + "FEATURE_INDICATORS_CHIP_QUANTUM_RESISTANCE", + tableName: "FeatureIndicatorsChip", + value: "Quantum resistance", + comment: "" + ) + } +} + +struct MultihopFeature: ChipFeature { + let settings: LatestTunnelSettings + var isEnabled: Bool { + settings.tunnelMultihopState.isEnabled + } + + var name: String { + NSLocalizedString( + "FEATURE_INDICATORS_CHIP_MULTIHOP", + tableName: "FeatureIndicatorsChip", + value: "Multihop", + comment: "" + ) + } +} + +struct ObfuscationFeature: ChipFeature { + let settings: LatestTunnelSettings + + var isEnabled: Bool { + settings.wireGuardObfuscation.state.isEnabled + } + + var name: String { + NSLocalizedString( + "FEATURE_INDICATORS_CHIP_OBFUSCATION", + tableName: "FeatureIndicatorsChip", + value: "Obfuscation", + comment: "" + ) + } +} + +struct DNSFeature: ChipFeature { + let settings: LatestTunnelSettings + + var isEnabled: Bool { + settings.dnsSettings.enableCustomDNS || !settings.dnsSettings.blockingOptions.isEmpty + } + + var name: String { + if !settings.dnsSettings.blockingOptions.isEmpty { + NSLocalizedString( + "FEATURE_INDICATORS_CHIP_CONTENT_BLOCKERS", + tableName: "FeatureIndicatorsChip", + value: "DNS content blockers", + comment: "" + ) + } else { + NSLocalizedString( + "FEATURE_INDICATORS_CHIP_CUSTOM_DNS", + tableName: "FeatureIndicatorsChip", + value: "Custom DNS", + comment: "" + ) + } + } +} + +struct IPOverrideFeature: ChipFeature { + let overrides: [IPOverride] + + var isEnabled: Bool { + !overrides.isEmpty + } + + var name: String { + NSLocalizedString( + "FEATURE_INDICATORS_CHIP_IP_OVERRIDE", + tableName: "FeatureIndicatorsChip", + value: "Server IP Override", + comment: "" + ) + } +} diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift index c1e990a1b1d9..a746897c06d2 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift @@ -11,5 +11,5 @@ import SwiftUI struct ChipModel: Identifiable { let id = UUID() - let name: LocalizedStringKey + let name: String } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift index 6d6614973f5f..1c6a5eb52267 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift @@ -11,20 +11,20 @@ import SwiftUI struct ChipView: View { let item: ChipModel var body: some View { - Text(item.name) + Text(LocalizedStringKey(item.name)) .font(.subheadline) .lineLimit(1) .foregroundStyle(UIColor.primaryTextColor.color) - .padding(.horizontal, 8) + .padding(.horizontal, UIMetrics.FeatureIndicators.chipViewHorisontalPadding) .padding(.vertical, 4) .background( - RoundedRectangle(cornerRadius: 8.0) + RoundedRectangle(cornerRadius: 8) .stroke( UIColor.primaryColor.color, lineWidth: 1 ) .background( - RoundedRectangle(cornerRadius: 8.0) + RoundedRectangle(cornerRadius: 8) .fill(UIColor.secondaryColor.color) ) ) @@ -33,7 +33,7 @@ struct ChipView: View { #Preview { ZStack { - ChipView(item: ChipModel(name: LocalizedStringKey("Example"))) + ChipView(item: ChipModel(name: "Example")) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(UIColor.secondaryColor.color) diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift index 65e3b0ccef38..fabec7f8c9e9 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift @@ -10,23 +10,54 @@ import SwiftUI protocol ChipViewModelProtocol: ObservableObject { var chips: [ChipModel] { get } - var isExpanded: Bool { get set } } -class MockFeatureIndicatorsViewModel: ChipViewModelProtocol { - @Published var chips: [ChipModel] = [ - ChipModel(name: LocalizedStringKey("DAITA")), - ChipModel(name: LocalizedStringKey("Obfuscation")), - ChipModel(name: LocalizedStringKey("Quantum resistance")), - ChipModel(name: LocalizedStringKey("Multihop")), - ChipModel(name: LocalizedStringKey("DNS content blockers")), - ChipModel(name: LocalizedStringKey("Custom DNS")), - ChipModel(name: LocalizedStringKey("Server IP override")), - ] +extension ChipViewModelProtocol { + func chipsToAdd(forContainerWidth containerWidth: CGFloat) -> (chips: [ChipModel], chipsWillOverflow: Bool) { + var chipsToAdd = [ChipModel]() + var chipsWillOverflow = false + + let moreTextWidth = "\(chips.count) more..." + .width(using: .preferredFont(forTextStyle: .subheadline)) + 4 // Some extra to be safe. + var totalChipsWidth: CGFloat = 0 + + for (index, chip) in chips.enumerated() { + let textWidth = chip.name.width(using: .preferredFont(forTextStyle: .subheadline)) + let chipWidth = textWidth + + UIMetrics.FeatureIndicators.chipViewHorisontalPadding * 2 + + UIMetrics.FeatureIndicators.chipViewTrailingMargin + let isLastChip = index == chips.count - 1 + + totalChipsWidth += chipWidth - @Published var isExpanded: Bool + let chipWillFitWithMoreText = (totalChipsWidth + moreTextWidth) <= containerWidth + let chipWillFit = totalChipsWidth <= containerWidth - init(isExpanded: Bool = false) { - self.isExpanded = isExpanded + if chipWillFitWithMoreText { + // If a chip can fit together with the "more" text, add it. + chipsToAdd.append(chip) + chipsWillOverflow = !isLastChip + } else if chipWillFit && isLastChip { + // If a chip can fit and it's the last one, add it. + chipsToAdd.append(chip) + chipsWillOverflow = false + } else { + break + } + } + + return (chipsToAdd, chipsWillOverflow) } } + +class MockFeatureIndicatorsViewModel: ChipViewModelProtocol { + @Published var chips: [ChipModel] = [ + ChipModel(name: "DAITA"), + ChipModel(name: "Obfuscation"), + ChipModel(name: "Quantum resistance"), + ChipModel(name: "Multihop"), + ChipModel(name: "DNS content blockers"), + ChipModel(name: "Custom DNS"), + ChipModel(name: "Server IP override"), + ] +} diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift index 3a1bf7d9afe3..d066bb47f21d 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift @@ -6,21 +6,23 @@ // Copyright © 2024 Mullvad VPN AB. All rights reserved. // -import MullvadSettings import SwiftUI -typealias ButtonAction = (ConnectionViewViewModel.TunnelControlAction) -> Void +typealias ButtonAction = (ConnectionViewViewModel.TunnelAction) -> Void struct ConnectionView: View { - @StateObject var viewModel: ConnectionViewViewModel + @StateObject var connectionViewModel: ConnectionViewViewModel @StateObject var indicatorsViewModel: FeatureIndicatorsViewModel + @State private(set) var isExpanded = false + var action: ButtonAction? var onContentUpdate: (() -> Void)? var body: some View { + Spacer() VStack(spacing: 22) { - if viewModel.showsActivityIndicator { + if connectionViewModel.showsActivityIndicator { CustomProgressView(style: .large) } @@ -28,63 +30,184 @@ struct ConnectionView: View { BlurView(style: .dark) VStack(alignment: .leading, spacing: 16) { - ConnectionPanel(viewModel: viewModel) + ConnectionHeader(viewModel: connectionViewModel, isExpanded: $isExpanded) - if !indicatorsViewModel.chips.isEmpty { - FeatureIndicatorsView(viewModel: indicatorsViewModel) + if connectionViewModel.showConnectionDetails { + ConnectionDetailsContainer( + viewModel: connectionViewModel, + indicatorsViewModel: indicatorsViewModel, + isExpanded: $isExpanded + ) } - ButtonPanel(viewModel: viewModel, action: action) + ButtonPanel(viewModel: connectionViewModel, action: action) } .padding(16) } .cornerRadius(12) .padding(16) } - .padding(.bottom, 8) // Adding some spacing so to not overlap with the map legal link. - .onReceive( - indicatorsViewModel.$isExpanded - .combineLatest( - viewModel.$tunnelState, - viewModel.$showsActivityIndicator - ) - ) { _ in + .padding(.bottom, 8) // Adding some spacing so as not to overlap with the map legal link. + .accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString) + .onChange(of: isExpanded) { _ in + onContentUpdate?() + } + .onReceive(connectionViewModel.combinedState) { _, _ in onContentUpdate?() + + if !connectionViewModel.showConnectionDetails { + isExpanded = false + } } } } -#Preview { - ConnectionView( - viewModel: ConnectionViewViewModel(tunnelState: .disconnected), - indicatorsViewModel: FeatureIndicatorsViewModel(tunnelSettings: LatestTunnelSettings(), ipOverrides: []) - ) { action in - print(action) - } - .background(UIColor.secondaryColor.color) +#Preview("ConnectionView (Normal)") { + ConnectionViewPreview(configuration: .normal).make() } -private struct ConnectionPanel: View { +#Preview("ConnectionView (Normal, no indicators)") { + ConnectionViewPreview(configuration: .normalNoIndicators).make() +} + +#Preview("ConnectionView (Expanded)") { + ConnectionViewPreview(configuration: .expanded).make() +} + +#Preview("ConnectionView (Expanded, no indicators)") { + ConnectionViewPreview(configuration: .expandedNoIndicators).make() +} + +private struct ConnectionHeader: View { @StateObject var viewModel: ConnectionViewViewModel + @Binding var isExpanded: Bool var body: some View { - VStack(alignment: .leading) { - Text(viewModel.localizedTitleForSecureLabel) - .textCase(.uppercase) - .font(.title3.weight(.semibold)) - .foregroundStyle(viewModel.textColorForSecureLabel.color) - .padding(.bottom, 4) - - if let countryAndCity = viewModel.titleForCountryAndCity, let server = viewModel.titleForServer { - Text(countryAndCity) + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 0) { + Text(viewModel.localizedTitleForSecureLabel) + .textCase(.uppercase) .font(.title3.weight(.semibold)) - .foregroundStyle(UIColor.primaryTextColor.color) - Text(server) - .font(.body) + .foregroundStyle(viewModel.textColorForSecureLabel.color) + .accessibilityIdentifier(viewModel.accessibilityIdForSecureLabel.asString) + + if let countryAndCity = viewModel.titleForCountryAndCity { + Text(countryAndCity) + .font(.title3.weight(.semibold)) + .foregroundStyle(UIColor.primaryTextColor.color) + .padding(.top, 4) + } + + if let server = viewModel.titleForServer { + Text(server) + .font(.body) + .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6)) + .padding(.top, 2) + } + } + .accessibilityLabel(viewModel.localizedAccessibilityLabelForSecureLabel) + + if viewModel.showConnectionDetails { + Spacer() + Image(.iconChevron) + .renderingMode(.template) + .rotationEffect(isExpanded ? .degrees(-90) : .degrees(90)) + .foregroundStyle(.white) + .transaction { transaction in + transaction.animation = nil + } + } + } + .accessibilityIdentifier(AccessibilityIdentifier.relayStatusCollapseButton.asString) + .contentShape(Rectangle()) + .onTapGesture { + isExpanded.toggle() + } + } +} + +private struct ConnectionDetailsContainer: View { + @StateObject var viewModel: ConnectionViewViewModel + @StateObject var indicatorsViewModel: FeatureIndicatorsViewModel + @Binding var isExpanded: Bool + + @State private var scrollViewHeight: CGFloat = 0 + + var body: some View { + if isExpanded { + Divider() + .background(UIColor.secondaryTextColor.color) + } + + // This geometry reader is somewhat of a workaround. It's "smart" in that it takes up as much + // space as it can and thereby helps the view to understand the maximum allowed height when + // placed in a UIKit context. If ConnectionView would ever be placed as a subview of SwiftUI + // parent, this reader could probably be removed. + GeometryReader { _ in + ScrollView { + VStack(spacing: 16) { + if !indicatorsViewModel.chips.isEmpty { + FeatureIndicatorsView( + viewModel: indicatorsViewModel, + isExpanded: $isExpanded + ) + } + + if isExpanded { + ConnectionDetails(viewModel: viewModel) + } + } + .sizeOfView { scrollViewHeight = $0.height } + } + } + .frame(maxHeight: scrollViewHeight) + } +} + +private struct ConnectionDetails: View { + @StateObject var viewModel: ConnectionViewViewModel + @State private var columnWidth: CGFloat = 0 + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(LocalizedStringKey("Connection details")) + .font(.footnote.weight(.semibold)) .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6)) + Spacer() + } + + VStack(alignment: .leading, spacing: 0) { + if let inAddress = viewModel.inAddress { + connectionDetailRow(title: LocalizedStringKey("In"), value: inAddress) + .accessibilityIdentifier(AccessibilityIdentifier.connectionPanelInAddressRow.asString) + } + if viewModel.tunnelIsConnected { + if let outAddressIpv4 = viewModel.outAddressIpv4 { + connectionDetailRow(title: LocalizedStringKey("Out IPv4"), value: outAddressIpv4) + .accessibilityIdentifier(AccessibilityIdentifier.connectionPanelOutAddressRow.asString) + } + if let outAddressIpv6 = viewModel.outAddressIpv6 { + connectionDetailRow(title: LocalizedStringKey("Out IPv6"), value: outAddressIpv6) + .accessibilityIdentifier(AccessibilityIdentifier.connectionPanelOutAddressRow.asString) + } + } } } - .accessibilityLabel(viewModel.localizedAccessibilityLabel) + } + + @ViewBuilder + private func connectionDetailRow(title: LocalizedStringKey, value: String) -> some View { + HStack(alignment: .top, spacing: 8) { + Text(title) + .font(.subheadline) + .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6)) + .frame(minWidth: columnWidth, alignment: .leading) + .sizeOfView { columnWidth = max(columnWidth, $0.width) } + Text(value) + .font(.subheadline) + .foregroundStyle(UIColor.primaryTextColor.color) + } } } @@ -95,29 +218,31 @@ private struct ButtonPanel: View { var body: some View { VStack(spacing: 16) { locationButton(with: action) + .disabled(viewModel.disableButtons) actionButton(with: action) + .disabled(viewModel.disableButtons) } } @ViewBuilder private func locationButton(with action: ButtonAction?) -> some View { - switch viewModel.tunnelState { + switch viewModel.tunnelStatus.state { case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error: SplitMainButton( text: viewModel.localizedTitleForSelectLocationButton, image: .iconReload, style: .default, - disabled: viewModel.disableButtons, primaryAction: { action?(.selectLocation) }, secondaryAction: { action?(.reconnect) } ) + .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString) case .disconnecting, .pendingReconnect, .disconnected: MainButton( text: viewModel.localizedTitleForSelectLocationButton, style: .default, - disabled: viewModel.disableButtons, action: { action?(.selectLocation) } ) + .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString) } } @@ -128,27 +253,31 @@ private struct ButtonPanel: View { MainButton( text: LocalizedStringKey("Connect"), style: .success, - disabled: viewModel.disableButtons, action: { action?(.connect) } ) + .accessibilityIdentifier(AccessibilityIdentifier.connectButton.asString) case .disconnect: MainButton( text: LocalizedStringKey("Disconnect"), style: .danger, - disabled: viewModel.disableButtons, action: { action?(.disconnect) } ) + .accessibilityIdentifier(AccessibilityIdentifier.disconnectButton.asString) case .cancel: MainButton( text: LocalizedStringKey( - viewModel.tunnelState == .waitingForConnectivity(.noConnection) + viewModel.tunnelStatus.state == .waitingForConnectivity(.noConnection) ? "Disconnect" : "Cancel" ), style: .danger, - disabled: viewModel.disableButtons, action: { action?(.cancel) } ) + .accessibilityIdentifier( + viewModel.tunnelStatus.state == .waitingForConnectivity(.noConnection) + ? AccessibilityIdentifier.disconnectButton.asString + : AccessibilityIdentifier.cancelButton.asString + ) } } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift new file mode 100644 index 000000000000..0575d4658fde --- /dev/null +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewPreview.swift @@ -0,0 +1,81 @@ +// +// ConnectionViewPreview.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-12-18. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadMockData +import MullvadSettings +import MullvadTypes +import PacketTunnelCore +import SwiftUI + +struct ConnectionViewPreview { + enum Configuration { + case normal, normalNoIndicators, expanded, expandedNoIndicators + } + + private let configuration: Configuration + + private let populatedTunnelSettings = LatestTunnelSettings( + wireGuardObfuscation: WireGuardObfuscationSettings(state: .udpOverTcp), + tunnelQuantumResistance: .on, + tunnelMultihopState: .on, + daita: DAITASettings(daitaState: .on) + ) + + private let viewModel = ConnectionViewViewModel( + tunnelStatus: TunnelStatus( + observedState: .connected(ObservedConnectionState( + selectedRelays: SelectedRelaysStub.selectedRelays, + relayConstraints: RelayConstraints(entryLocations: .any, exitLocations: .any, port: .any, filter: .any), + networkReachability: .reachable, + connectionAttemptCount: 0, + transportLayer: .udp, + remotePort: 80, + isPostQuantum: true, + isDaitaEnabled: true + )), + state: .connected(SelectedRelaysStub.selectedRelays, isPostQuantum: true, isDaita: true) + ) + ) + + init(configuration: Configuration) { + self.configuration = configuration + } + + @ViewBuilder + func make() -> some View { + VStack { + switch configuration { + case .normal: + connectionView(with: populatedTunnelSettings, viewModel: viewModel) + case .normalNoIndicators: + connectionView(with: LatestTunnelSettings(), viewModel: viewModel) + case .expanded: + connectionView(with: populatedTunnelSettings, viewModel: viewModel, isExpanded: true) + case .expandedNoIndicators: + connectionView(with: LatestTunnelSettings(), viewModel: viewModel, isExpanded: true) + } + } + .background(UIColor.secondaryColor.color) + } + + @ViewBuilder + private func connectionView( + with settings: LatestTunnelSettings, + viewModel: ConnectionViewViewModel, + isExpanded: Bool = false + ) -> some View { + ConnectionView( + connectionViewModel: viewModel, + indicatorsViewModel: FeatureIndicatorsViewModel( + tunnelSettings: settings, + ipOverrides: [] + ), + isExpanded: isExpanded + ) + } +} diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift index 29a4748b4100..2f7af8a3b572 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift @@ -6,16 +6,17 @@ // Copyright © 2024 Mullvad VPN AB. All rights reserved. // +import Combine import SwiftUI class ConnectionViewViewModel: ObservableObject { - enum TunnelControlActionButton { + enum TunnelActionButton { case connect case disconnect case cancel } - enum TunnelControlAction { + enum TunnelAction { case connect case disconnect case cancel @@ -23,42 +24,69 @@ class ConnectionViewViewModel: ObservableObject { case selectLocation } - @Published var tunnelState: TunnelState + @Published var tunnelStatus: TunnelStatus + @Published var outgoingConnectionInfo: OutgoingConnectionInfo? @Published var showsActivityIndicator = false - init(tunnelState: TunnelState) { - self.tunnelState = tunnelState + var combinedState: Publishers.CombineLatest< + Published.Publisher, + Published.Publisher + > { + $tunnelStatus.combineLatest($showsActivityIndicator) + } + + var tunnelIsConnected: Bool { + if case .connected = tunnelStatus.state { + true + } else { + false + } + } + + init(tunnelStatus: TunnelStatus) { + self.tunnelStatus = tunnelStatus } } extension ConnectionViewViewModel { + var showConnectionDetails: Bool { + switch tunnelStatus.state { + case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingEphemeralPeer, + .connected, .pendingReconnect, .waitingForConnectivity(.noNetwork): + true + case .disconnecting, .disconnected, .error: + false + } + } + var textColorForSecureLabel: UIColor { - switch tunnelState { - case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingEphemeralPeer: + switch tunnelStatus.state { + case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingEphemeralPeer, + .pendingReconnect, .disconnecting: .white case .connected: .successColor - case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error: + case .disconnected, .waitingForConnectivity(.noNetwork), .error: .dangerColor } } var disableButtons: Bool { - if case .waitingForConnectivity(.noNetwork) = tunnelState { - return true + if case .waitingForConnectivity(.noNetwork) = tunnelStatus.state { + true + } else { + false } - - return false } var localizedTitleForSecureLabel: LocalizedStringKey { - switch tunnelState { + switch tunnelStatus.state { case .connecting, .reconnecting, .negotiatingEphemeralPeer: - LocalizedStringKey("Connecting") + LocalizedStringKey("Connecting...") case .connected: LocalizedStringKey("Connected") case .disconnecting(.nothing): - LocalizedStringKey("Disconnecting") + LocalizedStringKey("Disconnecting...") case .disconnecting(.reconnect), .pendingReconnect: LocalizedStringKey("Reconnecting") case .disconnected: @@ -70,17 +98,19 @@ extension ConnectionViewViewModel { } } - var localizedTitleForSelectLocationButton: LocalizedStringKey { - switch tunnelState { - case .disconnecting, .pendingReconnect, .disconnected: - LocalizedStringKey("Select location") - case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error: - LocalizedStringKey("Switch location") + var accessibilityIdForSecureLabel: AccessibilityIdentifier { + switch tunnelStatus.state { + case .connected: + .connectionStatusConnectedLabel + case .connecting: + .connectionStatusConnectingLabel + default: + .connectionStatusNotConnectedLabel } } - var localizedAccessibilityLabel: LocalizedStringKey { - switch tunnelState { + var localizedAccessibilityLabelForSecureLabel: LocalizedStringKey { + switch tunnelStatus.state { case .disconnected, .waitingForConnectivity, .disconnecting, .pendingReconnect, .error: localizedTitleForSecureLabel case let .connected(tunnelInfo, _, _): @@ -98,8 +128,17 @@ extension ConnectionViewViewModel { } } - var actionButton: TunnelControlActionButton { - switch tunnelState { + var localizedTitleForSelectLocationButton: LocalizedStringKey { + switch tunnelStatus.state { + case .disconnecting, .pendingReconnect, .disconnected: + LocalizedStringKey("Select location") + case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error: + LocalizedStringKey("Switch location") + } + } + + var actionButton: TunnelActionButton { + switch tunnelStatus.state { case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork): .connect case .connecting, .pendingReconnect, .disconnecting(.reconnect), .waitingForConnectivity(.noConnection), @@ -111,7 +150,7 @@ extension ConnectionViewViewModel { } var titleForCountryAndCity: LocalizedStringKey? { - guard tunnelState.isSecured, let tunnelRelays = tunnelState.relays else { + guard let tunnelRelays = tunnelStatus.state.relays else { return nil } @@ -119,7 +158,7 @@ extension ConnectionViewViewModel { } var titleForServer: LocalizedStringKey? { - guard tunnelState.isSecured, let tunnelRelays = tunnelState.relays else { + guard let tunnelRelays = tunnelStatus.state.relays else { return nil } @@ -132,4 +171,50 @@ extension ConnectionViewViewModel { LocalizedStringKey("\(exitName)") } } + + var inAddress: String? { + guard let tunnelRelays = tunnelStatus.state.relays else { + return nil + } + + let observedTunnelState = tunnelStatus.observedState + + var portAndTransport = "" + if let inPort = observedTunnelState.connectionState?.remotePort { + let protocolLayer = observedTunnelState.connectionState?.transportLayer == .tcp ? "TCP" : "UDP" + portAndTransport = ":\(inPort) \(protocolLayer)" + } + + guard + let address = tunnelRelays.entry?.endpoint.ipv4Relay.ip + ?? tunnelStatus.state.relays?.exit.endpoint.ipv4Relay.ip + else { + return nil + } + + return "\(address)\(portAndTransport)" + } + + var outAddressIpv4: String? { + guard + let outgoingConnectionInfo, + let address = outgoingConnectionInfo.ipv4.exitIP ? outgoingConnectionInfo.ipv4.ip : nil + else { + return nil + } + + return "\(address)" + } + + var outAddressIpv6: String? { + guard + let outgoingConnectionInfo, + let ipv6 = outgoingConnectionInfo.ipv6, + let address = ipv6.exitIP ? ipv6.ip : nil + else { + return nil + } + + return "\(address)" + } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift index 9aed89004110..8d3f4c78d527 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift @@ -53,14 +53,14 @@ class FI_TunnelViewController: UIViewController, RootContainment { self.interactor = interactor tunnelState = interactor.tunnelStatus.state - connectionViewViewModel = ConnectionViewViewModel(tunnelState: tunnelState) + connectionViewViewModel = ConnectionViewViewModel(tunnelStatus: interactor.tunnelStatus) indicatorsViewViewModel = FeatureIndicatorsViewModel( tunnelSettings: interactor.tunnelSettings, ipOverrides: interactor.ipOverrides ) connectionView = ConnectionView( - viewModel: self.connectionViewViewModel, + connectionViewModel: self.connectionViewViewModel, indicatorsViewModel: self.indicatorsViewViewModel ) @@ -86,10 +86,15 @@ class FI_TunnelViewController: UIViewController, RootContainment { } interactor.didUpdateTunnelStatus = { [weak self] tunnelStatus in + self?.connectionViewViewModel.tunnelStatus = tunnelStatus self?.setTunnelState(tunnelStatus.state, animated: true) self?.view.setNeedsLayout() } + interactor.didGetOutGoingAddress = { [weak self] connectionInfo in + self?.connectionViewViewModel.outgoingConnectionInfo = connectionInfo + } + interactor.didUpdateTunnelSettings = { [weak self] tunnelSettings in self?.indicatorsViewViewModel.tunnelSettings = tunnelSettings } @@ -142,7 +147,6 @@ class FI_TunnelViewController: UIViewController, RootContainment { private func setTunnelState(_ tunnelState: TunnelState, animated: Bool) { self.tunnelState = tunnelState - connectionViewViewModel.tunnelState = tunnelState setNeedsHeaderBarStyleAppearanceUpdate() @@ -211,7 +215,7 @@ class FI_TunnelViewController: UIViewController, RootContainment { connectionController.didMove(toParent: self) view.addConstrainedSubviews([connectionViewProxy]) { - connectionViewProxy.pinEdgesToSuperview(.all().excluding(.top)) + connectionViewProxy.pinEdgesToSuperview(.all()) } } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift index eb1a29ea8195..b1a369f99eb0 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift @@ -10,22 +10,26 @@ import SwiftUI struct FeatureIndicatorsView: View where ViewModel: ChipViewModelProtocol { @ObservedObject var viewModel: ViewModel + @Binding var isExpanded: Bool var body: some View { VStack(alignment: .leading, spacing: 0) { - Text(LocalizedStringKey("Active features")) - .font(.footnote.weight(.semibold)) - .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6)) + if isExpanded { + Text(LocalizedStringKey("Active features")) + .font(.footnote.weight(.semibold)) + .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6)) + .padding(.bottom, 8) + } - ChipContainerView(viewModel: viewModel) - .onTapGesture { - viewModel.isExpanded.toggle() - } + ChipContainerView(viewModel: viewModel, isExpanded: $isExpanded) } } } -#Preview("FeatureIndicatorsView") { - FeatureIndicatorsView(viewModel: MockFeatureIndicatorsViewModel(isExpanded: true)) - .background(UIColor.secondaryColor.color) +#Preview { + FeatureIndicatorsView( + viewModel: MockFeatureIndicatorsViewModel(), + isExpanded: .constant(true) + ) + .background(UIColor.secondaryColor.color) } diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift index 42376b45608b..97eac59ca8f3 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift @@ -6,18 +6,16 @@ // Copyright © 2024 Mullvad VPN AB. All rights reserved. // -import Foundation import MullvadSettings +import SwiftUI class FeatureIndicatorsViewModel: ChipViewModelProtocol { @Published var tunnelSettings: LatestTunnelSettings @Published var ipOverrides: [IPOverride] - @Published var isExpanded = false - init(tunnelSettings: LatestTunnelSettings, ipOverrides: [IPOverride], isExpanded: Bool = false) { + init(tunnelSettings: LatestTunnelSettings, ipOverrides: [IPOverride]) { self.tunnelSettings = tunnelSettings self.ipOverrides = ipOverrides - self.isExpanded = isExpanded } var chips: [ChipModel] { diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index 88c933493b00..cff522297669 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -60,7 +60,7 @@ final class TunnelControlView: UIView { private let connectButton: AppButton = { let button = AppButton(style: .success) - button.setAccessibilityIdentifier(.secureConnectionButton) + button.setAccessibilityIdentifier(.connectButton) button.translatesAutoresizingMaskIntoConstraints = false return button }() @@ -115,7 +115,7 @@ final class TunnelControlView: UIView { backgroundColor = .clear directionalLayoutMargins = UIMetrics.contentLayoutMargins accessibilityContainerType = .semanticGroup - setAccessibilityIdentifier(.tunnelControlView) + setAccessibilityIdentifier(.connectionView) addSubviews() addButtonHandlers() diff --git a/ios/MullvadVPN/Views/MainButton.swift b/ios/MullvadVPN/Views/MainButton.swift index 679b34a2cd9d..a4240c433db6 100644 --- a/ios/MullvadVPN/Views/MainButton.swift +++ b/ios/MullvadVPN/Views/MainButton.swift @@ -11,7 +11,6 @@ import SwiftUI struct MainButton: View { var text: LocalizedStringKey var style: MainButtonStyle.Style - var disabled = false var action: () -> Void @@ -23,7 +22,7 @@ struct MainButton: View { Spacer() } }) - .buttonStyle(MainButtonStyle(style, disabled: disabled)) + .buttonStyle(MainButtonStyle(style)) .cornerRadius(UIMetrics.MainButton.cornerRadius) } } diff --git a/ios/MullvadVPN/Views/MainButtonStyle.swift b/ios/MullvadVPN/Views/MainButtonStyle.swift index f32a27fa068a..ceabd7761b10 100644 --- a/ios/MullvadVPN/Views/MainButtonStyle.swift +++ b/ios/MullvadVPN/Views/MainButtonStyle.swift @@ -10,7 +10,8 @@ import SwiftUI struct MainButtonStyle: ButtonStyle { var style: Style - @State var disabled: Bool + var disabled: Bool + @Environment(\.isEnabled) private var isEnabled: Bool init(_ style: Style, disabled: Bool = false) { self.style = style @@ -18,20 +19,19 @@ struct MainButtonStyle: ButtonStyle { } func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(.horizontal, 8) + return configuration.label .frame(height: 44) .foregroundColor( - disabled - ? UIColor.primaryTextColor.withAlphaComponent(0.2).color - : UIColor.primaryTextColor.color + isEnabled + ? UIColor.primaryTextColor.color + : UIColor.primaryTextColor.withAlphaComponent(0.2).color ) .background( - disabled - ? style.disabledColor - : configuration.isPressed + isEnabled + ? configuration.isPressed ? style.pressedColor : style.color + : style.disabledColor ) .font(.body.weight(.semibold)) } @@ -46,11 +46,11 @@ extension MainButtonStyle { var color: Color { switch self { case .default: - Color(UIColor.primaryColor) + UIColor.primaryColor.color case .danger: - Color(UIColor.dangerColor) + UIColor.dangerColor.color case .success: - Color(UIColor.successColor) + UIColor.successColor.color } } diff --git a/ios/MullvadVPN/Views/SplitMainButton.swift b/ios/MullvadVPN/Views/SplitMainButton.swift index 11336f424ba2..6f36b839befe 100644 --- a/ios/MullvadVPN/Views/SplitMainButton.swift +++ b/ios/MullvadVPN/Views/SplitMainButton.swift @@ -12,13 +12,12 @@ struct SplitMainButton: View { var text: LocalizedStringKey var image: ImageResource var style: MainButtonStyle.Style - var disabled = false + + @State private var secondaryButtonWidth: CGFloat = 0 var primaryAction: () -> Void var secondaryAction: () -> Void - @State private var width: CGFloat = 0 - var body: some View { HStack(spacing: 1) { Button(action: primaryAction, label: { @@ -27,18 +26,19 @@ struct SplitMainButton: View { Text(text) Spacer() } - .padding(.trailing, -width) + .padding(.trailing, -secondaryButtonWidth) }) Button(action: secondaryAction, label: { Image(image) .resizable() .scaledToFit() - .padding(4) + .frame(width: 24, height: 24) + .padding(10) }) .aspectRatio(1, contentMode: .fit) - .sizeOfView { width = $0.width } + .sizeOfView { secondaryButtonWidth = $0.width } } - .buttonStyle(MainButtonStyle(style, disabled: disabled)) + .buttonStyle(MainButtonStyle(style)) .cornerRadius(UIMetrics.MainButton.cornerRadius) } } diff --git a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift index 6929ee1f5ac5..da47a2c33404 100644 --- a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift +++ b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift @@ -74,7 +74,7 @@ class TunnelControlPage: Page { @discardableResult override init(_ app: XCUIApplication) { super.init(app) - self.pageElement = app.otherElements[.tunnelControlView] + self.pageElement = app.otherElements[.connectionView] waitForPageToBeShown() } @@ -84,7 +84,7 @@ class TunnelControlPage: Page { } @discardableResult func tapSecureConnectionButton() -> Self { - app.buttons[AccessibilityIdentifier.secureConnectionButton].tap() + app.buttons[AccessibilityIdentifier.connectButton].tap() return self } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index d9cce79f576d..422249b7d65f 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -99,6 +99,7 @@ public actor PacketTunnelActor { } } + // swiftlint:disable:next function_body_length func executeEffect(_ effect: Effect) async { switch effect { case .startDefaultPathObserver: @@ -136,7 +137,6 @@ public actor PacketTunnelActor { state = .disconnected case let .configureForErrorState(reason): await setErrorStateInternal(with: reason) - case let .cacheActiveKey(lastKeyRotation): cacheActiveKey(lastKeyRotation: lastKeyRotation) case let .reconfigureForEphemeralPeer(configuration, configurationSemaphore): diff --git a/ios/convert-assets.rb b/ios/convert-assets.rb index 5419e6282d9e..637068cfe668 100755 --- a/ios/convert-assets.rb +++ b/ios/convert-assets.rb @@ -32,7 +32,6 @@ "icon-extLink.svg", "icon-fail.svg", "icon-info.svg", - "icon-reload.svg", "icon-settings.svg", "icon-spinner.svg", "icon-success.svg",