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/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 54314e6ed8b8..acdb367eaf7a 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 */; }; @@ -704,7 +706,6 @@ 85EC620C2B838D10005AFFB5 /* MullvadAPIWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B132B5983CF00795FE1 /* MullvadAPIWrapper.swift */; }; 85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FB5A0B2B6903990015DCED /* WelcomePage.swift */; }; 85FB5A102B6960A30015DCED /* AccountDeletionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FB5A0F2B6960A30015DCED /* AccountDeletionPage.swift */; }; - A90216DB2D0C3E03001626E3 /* HeaderBarSwiftUIHostedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90216DA2D0C3E03001626E3 /* HeaderBarSwiftUIHostedView.swift */; }; A90763B02B2857D50045ADF0 /* Socks5ConnectCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A02B2857D50045ADF0 /* Socks5ConnectCommand.swift */; }; A90763B12B2857D50045ADF0 /* Socks5Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A12B2857D50045ADF0 /* Socks5Endpoint.swift */; }; A90763B22B2857D50045ADF0 /* Socks5EndpointReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A22B2857D50045ADF0 /* Socks5EndpointReader.swift */; }; @@ -756,7 +757,6 @@ A988A3E22AFE54AC0008D2C7 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; }; A988DF272ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */; }; A988DF2A2ADE880300D807EF /* TunnelSettingsV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */; }; - A98E31752D0B1CDC00C092B7 /* FeatureIndicatorsScrollContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98E31742D0B1CDC00C092B7 /* FeatureIndicatorsScrollContainerView.swift */; }; A992DA202C24709F00DE7CE5 /* MullvadRustRuntime.h in Headers */ = {isa = PBXBuildFile; fileRef = A992DA1F2C24709F00DE7CE5 /* MullvadRustRuntime.h */; settings = {ATTRIBUTES = (Public, ); }; }; A992DA232C24709F00DE7CE5 /* MullvadRustRuntime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A992DA1D2C24709F00DE7CE5 /* MullvadRustRuntime.framework */; }; A992DA242C24709F00DE7CE5 /* MullvadRustRuntime.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A992DA1D2C24709F00DE7CE5 /* MullvadRustRuntime.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1005,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 */; }; @@ -2024,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 = ""; }; @@ -2085,7 +2087,6 @@ A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DevicesProxy+Stubs.swift"; sourceTree = ""; }; A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIProxy+Stubs.swift"; sourceTree = ""; }; A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessTokenManager+Stubs.swift"; sourceTree = ""; }; - A90216DA2D0C3E03001626E3 /* HeaderBarSwiftUIHostedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarSwiftUIHostedView.swift; sourceTree = ""; }; A90763A02B2857D50045ADF0 /* Socks5ConnectCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5ConnectCommand.swift; sourceTree = ""; }; A90763A12B2857D50045ADF0 /* Socks5Endpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5Endpoint.swift; sourceTree = ""; }; A90763A22B2857D50045ADF0 /* Socks5EndpointReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socks5EndpointReader.swift; sourceTree = ""; }; @@ -2140,7 +2141,6 @@ A98502022B627B120061901E /* LocalNetworkProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNetworkProbe.swift; sourceTree = ""; }; A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardObfuscationSettings.swift; sourceTree = ""; }; A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV3.swift; sourceTree = ""; }; - A98E31742D0B1CDC00C092B7 /* FeatureIndicatorsScrollContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIndicatorsScrollContainerView.swift; sourceTree = ""; }; A98F1B502C19C48D003C869E /* EphemeralPeerExchangeActorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerExchangeActorTests.swift; sourceTree = ""; }; A992DA1D2C24709F00DE7CE5 /* MullvadRustRuntime.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadRustRuntime.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A992DA1F2C24709F00DE7CE5 /* MullvadRustRuntime.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadRustRuntime.h; sourceTree = ""; }; @@ -2253,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 = ""; }; @@ -3642,7 +3642,6 @@ A9D9A4C12C36D53C004088DD /* MullvadRustRuntimeTests */, 58CE5E61224146200008646E /* Products */, 584F991F2902CBDD001F858D /* Frameworks */, - 7A0EAE982D01B29E00D3EB8B /* Recovered References */, ); sourceTree = ""; }; @@ -3943,13 +3942,6 @@ path = Edit; sourceTree = ""; }; - 7A0EAE982D01B29E00D3EB8B /* Recovered References */ = { - isa = PBXGroup; - children = ( - ); - name = "Recovered References"; - sourceTree = ""; - }; 7A2960F72A964A3500389B82 /* Alert */ = { isa = PBXGroup; children = ( @@ -4086,14 +4078,13 @@ children = ( F0ADF1CF2D01B50B00299F09 /* ChipView */, 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */, - F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */, + F0B495792D02F41F00CFEC2A /* ChipFeature.swift */, 7AA130982CFF365A00640DF9 /* ConnectionView.swift */, + 7AF84F472D12C9CF00C72690 /* ConnectionViewPreview.swift */, 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */, F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */, F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */, 7AFBE3882D08915D002335FC /* FI_TunnelViewController.swift */, - A98E31742D0B1CDC00C092B7 /* FeatureIndicatorsScrollContainerView.swift */, - A90216DA2D0C3E03001626E3 /* HeaderBarSwiftUIHostedView.swift */, ); path = FeatureIndicators; sourceTree = ""; @@ -4402,6 +4393,7 @@ F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */, 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */, A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */, + 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */, ); path = MullvadREST; sourceTree = ""; @@ -5941,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 */, @@ -6119,7 +6112,6 @@ 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, F0C6FA852A6A733700F521F0 /* InAppPurchaseInteractor.swift in Sources */, 58CEB2F92AFD136E00E6E088 /* UIBackgroundConfiguration+Extensions.swift in Sources */, - A98E31752D0B1CDC00C092B7 /* FeatureIndicatorsScrollContainerView.swift in Sources */, 5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */, A98502032B627B120061901E /* LocalNetworkProbe.swift in Sources */, 7A6F2FA92AFD0842006D0856 /* CustomDNSDataSource.swift in Sources */, @@ -6137,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 */, @@ -6231,7 +6223,6 @@ 5827B0BF2B14B37D00CCBBA1 /* Publisher+PreviousValue.swift in Sources */, 7A9CCCB62A96302800DD6A34 /* OutOfTimeCoordinator.swift in Sources */, 5827B0AA2B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift in Sources */, - A90216DB2D0C3E03001626E3 /* HeaderBarSwiftUIHostedView.swift in Sources */, F050AE5E2B739A73003F4EDB /* LocationDataSourceProtocol.swift in Sources */, A99E5EE22B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift in Sources */, 7A5869A22B502EA800640D27 /* MethodSettingsSectionIdentifier.swift in Sources */, @@ -6522,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..d4877ec2fda1 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 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/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 d58fb05aa5f8..000000000000 Binary files a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf and /dev/null differ 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/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeature.swift similarity index 68% rename from ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift rename to ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeature.swift index c005b3f080ed..4dce7dc27899 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeature.swift @@ -1,17 +1,16 @@ // -// ChipFeatures.swift +// ChipFeature.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 } + var name: String { get } } struct DaitaFeature: ChipFeature { @@ -21,8 +20,8 @@ struct DaitaFeature: ChipFeature { settings.daita.daitaState.isEnabled } - var name: LocalizedStringKey { - LocalizedStringKey("DAITA") + var name: String { + String("DAITA") } } @@ -32,8 +31,8 @@ struct QuantumResistanceFeature: ChipFeature { settings.tunnelQuantumResistance.isEnabled } - var name: LocalizedStringKey { - LocalizedStringKey("Quantum resistance") + var name: String { + String("Quantum resistance") } } @@ -43,8 +42,8 @@ struct MultihopFeature: ChipFeature { settings.tunnelMultihopState.isEnabled } - var name: LocalizedStringKey { - LocalizedStringKey("Multihop") + var name: String { + String("Multihop") } } @@ -55,8 +54,8 @@ struct ObfuscationFeature: ChipFeature { settings.wireGuardObfuscation.state.isEnabled } - var name: LocalizedStringKey { - LocalizedStringKey("Obfuscation") + var name: String { + String("Obfuscation") } } @@ -67,11 +66,11 @@ struct DNSFeature: ChipFeature { settings.dnsSettings.enableCustomDNS || !settings.dnsSettings.blockingOptions.isEmpty } - var name: LocalizedStringKey { + var name: String { if !settings.dnsSettings.blockingOptions.isEmpty { - return LocalizedStringKey("DNS content blockers") + return String("DNS content blockers") } - return LocalizedStringKey("Custom DNS") + return String("Custom DNS") } } @@ -82,7 +81,7 @@ struct IPOverrideFeature: ChipFeature { !overrides.isEmpty } - var name: LocalizedStringKey { - LocalizedStringKey("Server IP override") + var name: String { + String("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..e40e9a7ef27a 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift @@ -10,46 +10,43 @@ 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 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 - HStack { + var chipsToAdd = viewModel.chips + var showMoreButton = false + + if !isExpanded { + (chipsToAdd, showMoreButton) = viewModel.chipsToAdd(forContainerWidth: containerWidth) + } + + return HStack { ZStack(alignment: .topLeading) { - createChipViews(chips: Array(viewModel.chips.prefix(numberOfChips)), containerWidth: containerWidth) + if isExpanded { + createChipViews(chips: viewModel.chips, containerWidth: containerWidth) + } else { + 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) } 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, -5) } private func createChipViews(chips: [ChipModel], containerWidth: CGFloat) -> some View { @@ -65,7 +62,7 @@ struct ChipContainerView: View where ViewModel: ChipViewModelProtocol 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 +71,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/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..42e0e508cac9 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(.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..f02218da3d7b 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift @@ -10,23 +10,51 @@ 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)) + var totalChipsWidth: CGFloat = 0 + + for (index, chip) in chips.enumerated() { + let textWidth = chip.name.width(using: .preferredFont(forTextStyle: .subheadline)) + let chipWidth = textWidth + 16 /* inside horisontal padding */ + 8 /* outside trailing padding */ + 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 01a90e2bf448..7def0f0f532f 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift @@ -17,13 +17,15 @@ typealias ButtonAction = (ConnectionViewViewModel.TunnelControlAction) -> Void struct ConnectionView: View { @StateObject var viewModel: ConnectionViewViewModel @StateObject var indicatorsViewModel: FeatureIndicatorsViewModel - @State var expandConnectionDetails = false + + @State private(set) var isExpanded = false + @State private var scrollViewHeight: CGFloat = 0 var action: ButtonAction? var onContentUpdate: (() -> Void)? - var onChevronToggle: (() -> Void)? var body: some View { + Spacer() VStack(spacing: 22) { if viewModel.showsActivityIndicator { CustomProgressView(style: .large) @@ -33,22 +35,37 @@ struct ConnectionView: View { BlurView(style: .dark) VStack(alignment: .leading, spacing: 16) { - ConnectionPanel(viewModel: viewModel) + ConnectionInfo(viewModel: viewModel, isExpanded: $isExpanded) + + if isExpanded { + Divider() + .background(UIColor.secondaryTextColor.color) + } - if !indicatorsViewModel.chips.isEmpty { - FeatureIndicatorsView(viewModel: indicatorsViewModel) + // 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. + if viewModel.showConnectionDetails { + 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) } - ConnectionPanel(viewModel: viewModel, onChevronToggle: { - expandConnectionDetails.toggle() - }, isExpanded: $expandConnectionDetails) - Divider() - .background(UIColor.secondaryTextColor.color) - FeatureIndicatorsScrollContainerView( - isExpanded: $expandConnectionDetails, - content: { Text("Hello") } - ) - .frame(maxWidth: .infinity) - .border(.white) ButtonPanel(viewModel: viewModel, action: action) } @@ -57,103 +74,123 @@ struct ConnectionView: View { .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. + .onChange(of: isExpanded) { _ in onContentUpdate?() } - } -} + .onReceive(viewModel.combinedState) { _, _ in + onContentUpdate?() -#Preview { - ConnectionView( - viewModel: ConnectionViewViewModel(tunnelState: .disconnected), - indicatorsViewModel: FeatureIndicatorsViewModel(tunnelSettings: LatestTunnelSettings(), ipOverrides: []) - ) { action in - print(action) - let selectedRelays = SelectedRelays( - entry: nil, - exit: SelectedRelay( - endpoint: MullvadEndpoint( - ipv4Relay: IPv4Endpoint(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 - ) - let connectedState = TunnelState.connected(selectedRelays, isPostQuantum: true, isDaita: true) - - return ZStack { - VStack { - HeaderBarSwiftUIHostedView() - .frame(maxHeight: 100) - ConnectionView( - viewModel: ConnectionViewViewModel(tunnelState: connectedState), - action: { action in print(action) }, - onContentUpdate: { print("On content Update") }, - onChevronToggle: { print("Chevron toggle") } - ) + if !viewModel.showConnectionDetails { + isExpanded = false + } } } - .background(UIColor.secondaryColor.color) } -private struct ConnectionPanel: View { +#Preview("ConnectionView (Normal)") { + ConnectionViewPreview(configuration: .normal).make() +} + +#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 ConnectionInfo: View { @StateObject var viewModel: ConnectionViewViewModel - var onChevronToggle: (() -> Void)? - var isExpanded: Binding + @Binding var isExpanded: Bool var body: some View { HStack(alignment: .top) { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 0) { 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) .font(.title3.weight(.semibold)) .foregroundStyle(UIColor.primaryTextColor.color) + .padding(.top, 4) Text(server) .font(.body) .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6)) + .padding(.top, 2) } } .accessibilityLabel(viewModel.localizedAccessibilityLabel) - if case .connected = viewModel.tunnelState { - if let onChevronToggle { - Spacer() - Button(action: onChevronToggle) { + + if viewModel.showConnectionDetails { + Spacer() + Button( + action: { isExpanded.toggle() }, + label: { Image(.iconChevron) .renderingMode(.template) - .rotationEffect(isExpanded.wrappedValue ? .degrees(-90) : .degrees(90)) + .rotationEffect(isExpanded ? .degrees(-90) : .degrees(90)) .frame(width: 44, height: 44, alignment: .topTrailing) .foregroundStyle(.white) .transaction { transaction in transaction.animation = nil } } + ) + } + } + } +} + +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) + } + if viewModel.tunnelIsConnected { + if let outAddressIpv4 = viewModel.outAddressIpv4 { + connectionDetailRow(title: LocalizedStringKey("Out IPv4"), value: outAddressIpv4) + } + if let outAddressIpv6 = viewModel.outAddressIpv6 { + connectionDetailRow(title: LocalizedStringKey("Out IPv6"), value: outAddressIpv6) + } } } } } + + @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) + } + } } private struct ButtonPanel: View { @@ -169,7 +206,7 @@ private struct ButtonPanel: View { @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, @@ -179,6 +216,7 @@ private struct ButtonPanel: View { primaryAction: { action?(.selectLocation) }, secondaryAction: { action?(.reconnect) } ) + .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString) case .disconnecting, .pendingReconnect, .disconnected: MainButton( text: viewModel.localizedTitleForSelectLocationButton, @@ -186,6 +224,7 @@ private struct ButtonPanel: View { disabled: viewModel.disableButtons, action: { action?(.selectLocation) } ) + .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString) } } @@ -199,6 +238,7 @@ private struct ButtonPanel: View { disabled: viewModel.disableButtons, action: { action?(.connect) } ) + .accessibilityIdentifier(AccessibilityIdentifier.connectButton.asString) case .disconnect: MainButton( text: LocalizedStringKey("Disconnect"), @@ -206,10 +246,11 @@ private struct ButtonPanel: View { 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" ), @@ -217,6 +258,11 @@ private struct ButtonPanel: View { 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..bacfbd546f1d --- /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( + viewModel: 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..b94da671e6e5 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift @@ -6,6 +6,7 @@ // Copyright © 2024 Mullvad VPN AB. All rights reserved. // +import Combine import SwiftUI class ConnectionViewViewModel: ObservableObject { @@ -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: @@ -71,7 +99,7 @@ extension ConnectionViewViewModel { } var localizedTitleForSelectLocationButton: LocalizedStringKey { - switch tunnelState { + switch tunnelStatus.state { case .disconnecting, .pendingReconnect, .disconnected: LocalizedStringKey("Select location") case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error: @@ -80,7 +108,7 @@ extension ConnectionViewViewModel { } var localizedAccessibilityLabel: LocalizedStringKey { - switch tunnelState { + switch tunnelStatus.state { case .disconnected, .waitingForConnectivity, .disconnecting, .pendingReconnect, .error: localizedTitleForSecureLabel case let .connected(tunnelInfo, _, _): @@ -99,7 +127,7 @@ extension ConnectionViewViewModel { } var actionButton: TunnelControlActionButton { - switch tunnelState { + switch tunnelStatus.state { case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork): .connect case .connecting, .pendingReconnect, .disconnecting(.reconnect), .waitingForConnectivity(.noConnection), @@ -111,7 +139,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 +147,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 +160,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..610086efa849 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift @@ -53,7 +53,7 @@ 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 @@ -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/FeatureIndicatorsScrollContainerView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsScrollContainerView.swift deleted file mode 100644 index c4a5ff0f2355..000000000000 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsScrollContainerView.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// FeatureIndicatorsScrollContainerView.swift -// MullvadVPN -// -// Created by Marco Nikic on 2024-12-12. -// Copyright © 2024 Mullvad VPN AB. All rights reserved. -// - -import SwiftUI - -struct FeatureIndicatorsScrollContainerView: View { - var isExpanded: Binding - @ViewBuilder - let content: ContentView - - var body: some View { - ScrollView { - content - .frame(maxWidth: .infinity) - .background(.blue) - } - .frame(maxHeight: isExpanded.wrappedValue ? .infinity : 40) - } -} - -#Preview { - ExampleView().background(UIColor.secondaryColor.color) -} - -private struct ExampleView: View { - @State var isExpanded = false - - var body: some View { - VStack { - Button(action: { - isExpanded.toggle() - }, label: { - Text("Toggle layout") - }) - FeatureIndicatorsScrollContainerView(isExpanded: $isExpanded) { - if isExpanded { - BigLayoutView() - } else { - SmallLayoutView() - } - } - } - } -} - -private struct SmallLayoutView: View { - var body: some View { - HStack { - ForEach(0 ..< 3) { index in - Text("hehehjr \(index)") - } - } - } -} - -private struct BigLayoutView: View { - var body: some View { - Group { - VStack { - ForEach(0 ..< 5) { _ in - HStack { - ForEach(0 ..< 8) { index in - Text("hello \(index)") - } - } - } - } - } - } -} diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift index eb1a29ea8195..cfdd533cd367 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift @@ -10,22 +10,29 @@ 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) + } + .onTapGesture { + isExpanded.toggle() } } } -#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/FeatureIndicators/HeaderBarSwiftUIHostedView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/HeaderBarSwiftUIHostedView.swift deleted file mode 100644 index dc5ddde6209e..000000000000 --- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/HeaderBarSwiftUIHostedView.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// HeaderBarSwiftUIHostedView.swift -// MullvadVPN -// -// Created by Marco Nikic on 2024-12-13. -// Copyright © 2024 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import SwiftUI - -struct HeaderBarSwiftUIHostedView: UIViewRepresentable { - typealias UIViewType = HeaderBarView - - func makeUIView(context: Context) -> HeaderBarView { - let headerBarView = HeaderBarView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) - headerBarView.translatesAutoresizingMaskIntoConstraints = false - headerBarView.insetsLayoutMarginsFromSafeArea = false - - var headerBarPresentation = HeaderBarPresentation.default - headerBarView.backgroundColor = headerBarPresentation.style.backgroundColor() - headerBarView.showsDivider = headerBarPresentation.showsDivider - - return headerBarView - } - - func updateUIView(_ uiView: HeaderBarView, context: Context) { - print("update") - } -} diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index 88c933493b00..043671ec49a0 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 }() diff --git a/ios/MullvadVPN/Views/MainButton.swift b/ios/MullvadVPN/Views/MainButton.swift index 679b34a2cd9d..3cb0046deb75 100644 --- a/ios/MullvadVPN/Views/MainButton.swift +++ b/ios/MullvadVPN/Views/MainButton.swift @@ -11,7 +11,8 @@ import SwiftUI struct MainButton: View { var text: LocalizedStringKey var style: MainButtonStyle.Style - var disabled = false + + @State var disabled = false var action: () -> Void diff --git a/ios/MullvadVPN/Views/MainButtonStyle.swift b/ios/MullvadVPN/Views/MainButtonStyle.swift index f32a27fa068a..78717d50a8f6 100644 --- a/ios/MullvadVPN/Views/MainButtonStyle.swift +++ b/ios/MullvadVPN/Views/MainButtonStyle.swift @@ -19,7 +19,6 @@ struct MainButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label - .padding(.horizontal, 8) .frame(height: 44) .foregroundColor( disabled diff --git a/ios/MullvadVPN/Views/SplitMainButton.swift b/ios/MullvadVPN/Views/SplitMainButton.swift index 11336f424ba2..80e58ca21a1e 100644 --- a/ios/MullvadVPN/Views/SplitMainButton.swift +++ b/ios/MullvadVPN/Views/SplitMainButton.swift @@ -12,13 +12,13 @@ struct SplitMainButton: View { var text: LocalizedStringKey var image: ImageResource var style: MainButtonStyle.Style - var disabled = false + + @State 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,16 +27,17 @@ 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)) .cornerRadius(UIMetrics.MainButton.cornerRadius) 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",