summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-01-13 09:42:48 +0100
committerJon Petersson <jon.petersson@mullvad.net>2025-01-13 09:42:48 +0100
commitfaf8f909b83af3401920485dbfc816ffd55d5cb7 (patch)
tree2208c88884ecaf850b62631cc8d29b21726ae8fd
parentcde728046d456248549f11a790a3a4916848bb18 (diff)
parent11ce559117481460356881680dfb500821d79ead (diff)
downloadmullvadvpn-faf8f909b83af3401920485dbfc816ffd55d5cb7.tar.xz
mullvadvpn-faf8f909b83af3401920485dbfc816ffd55d5cb7.zip
Merge branch 'add-connection-detail-toggle-to-connection-view-ios-961'
-rw-r--r--ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift36
-rw-r--r--ios/MullvadRustRuntime/EphemeralPeerNegotiator.swift3
-rw-r--r--ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift3
-rw-r--r--ios/MullvadSettings/IPOverrideRepository.swift10
-rw-r--r--ios/MullvadSettings/WireGuardObfuscationSettings.swift4
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj84
-rw-r--r--ios/MullvadVPN/Classes/AccessbilityIdentifier.swift5
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift3
-rw-r--r--ios/MullvadVPN/Coordinators/TunnelCoordinator.swift16
-rw-r--r--ios/MullvadVPN/Extensions/String+Helpers.swift6
-rw-r--r--ios/MullvadVPN/Extensions/View+Conditionals.swift41
-rw-r--r--ios/MullvadVPN/Extensions/View+TapAreaSize.swift5
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/Contents.json2
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf71
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/icon-reload.svg10
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift5
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift140
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift90
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipContainerView.swift95
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipFeature.swift127
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipModel.swift15
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipView.swift40
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipViewModelProtocol.swift65
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift89
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewComponentPreview.swift68
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewViewModel.swift (renamed from ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift)140
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift57
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift64
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift63
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift51
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift33
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift35
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift4
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift2
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift31
-rw-r--r--ios/MullvadVPN/Views/MainButton.swift3
-rw-r--r--ios/MullvadVPN/Views/MainButtonStyle.swift38
-rw-r--r--ios/MullvadVPN/Views/SplitMainButton.swift19
-rw-r--r--ios/MullvadVPNTests/MullvadSettings/IPOverrideRepositoryStub.swift6
-rw-r--r--ios/MullvadVPNUITests/Pages/TunnelControlPage.swift4
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor.swift2
-rwxr-xr-xios/convert-assets.rb1
42 files changed, 1271 insertions, 315 deletions
diff --git a/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift b/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift
new file mode 100644
index 0000000000..adc7492230
--- /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 8346b2686d..85503c8bf4 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 52e4742c21..ff7a3f74a4 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/MullvadSettings/IPOverrideRepository.swift b/ios/MullvadSettings/IPOverrideRepository.swift
index 867a1c077f..441ff6c35e 100644
--- a/ios/MullvadSettings/IPOverrideRepository.swift
+++ b/ios/MullvadSettings/IPOverrideRepository.swift
@@ -6,10 +6,11 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
-import Foundation
+import Combine
import MullvadLogging
public protocol IPOverrideRepositoryProtocol {
+ var overridesPublisher: AnyPublisher<[IPOverride], Never> { get }
func add(_ overrides: [IPOverride])
func fetchAll() -> [IPOverride]
func deleteAll()
@@ -17,6 +18,11 @@ public protocol IPOverrideRepositoryProtocol {
}
public class IPOverrideRepository: IPOverrideRepositoryProtocol {
+ private let overridesSubject: CurrentValueSubject<[IPOverride], Never> = .init([])
+ public var overridesPublisher: AnyPublisher<[IPOverride], Never> {
+ overridesSubject.eraseToAnyPublisher()
+ }
+
private let logger = Logger(label: "IPOverrideRepository")
private let readWriteLock = NSLock()
@@ -58,6 +64,7 @@ public class IPOverrideRepository: IPOverrideRepositoryProtocol {
do {
try readWriteLock.withLock {
try SettingsManager.store.delete(key: .ipOverrides)
+ overridesSubject.send([])
}
} catch {
logger.error("Could not delete all overrides. \nError: \(error)")
@@ -85,6 +92,7 @@ public class IPOverrideRepository: IPOverrideRepositoryProtocol {
try readWriteLock.withLock {
try SettingsManager.store.write(data, for: .ipOverrides)
+ overridesSubject.send(overrides)
}
}
diff --git a/ios/MullvadSettings/WireGuardObfuscationSettings.swift b/ios/MullvadSettings/WireGuardObfuscationSettings.swift
index f067114cc6..c52a637626 100644
--- a/ios/MullvadSettings/WireGuardObfuscationSettings.swift
+++ b/ios/MullvadSettings/WireGuardObfuscationSettings.swift
@@ -46,6 +46,10 @@ public enum WireGuardObfuscationState: Codable {
self = .off
}
}
+
+ public var isEnabled: Bool {
+ [.udpOverTcp, .shadowsocks].contains(self)
+ }
}
public enum WireGuardObfuscationUdpOverTcpPort: Codable, Equatable, CustomStringConvertible {
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index b5f13b415f..e1cda675d7 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -43,6 +43,9 @@
44075DFB2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44075DFA2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift */; };
440E5AB02CDBD67D00B09614 /* StatefulPreviewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440E5AAF2CDBD67D00B09614 /* StatefulPreviewWrapper.swift */; };
440E5AB42CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440E5AB32CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift */; };
+ 4419AA892D282687001B13C9 /* DetailsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4419AA882D282687001B13C9 /* DetailsContainer.swift */; };
+ 4419AA8B2D2826E5001B13C9 /* DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4419AA8A2D2826E5001B13C9 /* DetailsView.swift */; };
+ 4419AA8E2D2828A4001B13C9 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4419AA8D2D2828A4001B13C9 /* HeaderView.swift */; };
4422C0712CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4422C0702CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift */; };
4424CDD32CDBD4A6009D8C9F /* SingleChoiceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4424CDD22CDBD4A6009D8C9F /* SingleChoiceList.swift */; };
447F3D8A2CDE1853006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447F3D882CDE1852006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift */; };
@@ -52,6 +55,8 @@
4495ECD52D131A4800A7358B /* ShadowsocksObfuscationSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4495ECD42D131A3E00A7358B /* ShadowsocksObfuscationSettingsPage.swift */; };
449872E12B7BBC5400094DDC /* TunnelSettingsUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */; };
449872E42B7CB96300094DDC /* TunnelSettingsUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */; };
+ 449E9A6D2D283A2500F8574A /* ConnectionViewComponentPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449E9A6C2D283A2500F8574A /* ConnectionViewComponentPreview.swift */; };
+ 449E9A6F2D283C7400F8574A /* ButtonPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449E9A6E2D283C7400F8574A /* ButtonPanel.swift */; };
449EBA262B975B9700DFA4EB /* EphemeralPeerReceiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EBA252B975B9700DFA4EB /* EphemeralPeerReceiving.swift */; };
44B02E3B2BC5732D008EDF34 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B02E3A2BC5732D008EDF34 /* LoggingTests.swift */; };
44B02E3C2BC5B8A5008EDF34 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; };
@@ -616,6 +621,7 @@
7AA1309F2D007B2500640DF9 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA1309E2D007B2500640DF9 /* VisualEffectView.swift */; };
7AA130A12D01B1E200640DF9 /* SplitMainButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA130A02D01B1E200640DF9 /* SplitMainButton.swift */; };
7AA513862BC91C6B00D081A4 /* LogRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */; };
+ 7AA636382D2D3BB0009B2C89 /* View+Conditionals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA636372D2D3BAC009B2C89 /* View+Conditionals.swift */; };
7AA7046A2C8EFE2B0045699D /* StoredRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA704682C8EFE050045699D /* StoredRelays.swift */; };
7AB2B6702BA1EB8C00B03E3B /* ListCustomListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */; };
7AB2B6712BA1EB8C00B03E3B /* ListCustomListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */; };
@@ -655,6 +661,7 @@
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 */; };
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 */; };
@@ -1001,7 +1008,14 @@
F0ADC3722CD3AD1600A1AD97 /* ChipCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADC3712CD3AD1600A1AD97 /* ChipCollectionView.swift */; };
F0ADC3742CD3C47400A1AD97 /* ChipFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADC3732CD3C47400A1AD97 /* ChipFlowLayout.swift */; };
F0ADF1CD2CFDFF3100299F09 /* StringConversionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADF1CC2CFDFF3100299F09 /* StringConversionError.swift */; };
+ F0ADF1D12D01B55C00299F09 /* ChipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADF1D02D01B55C00299F09 /* ChipModel.swift */; };
+ F0ADF1D32D01B6B400299F09 /* FeatureIndicatorsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */; };
+ F0ADF1D52D01DCFD00299F09 /* ChipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ADF1D42D01DCFD00299F09 /* ChipView.swift */; };
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 /* 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 */; };
F0B894F32BF7526700817A42 /* RelaySelector+Wireguard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */; };
@@ -1434,6 +1448,9 @@
44075DFA2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscationSettingsViewModel.swift; sourceTree = "<group>"; };
440E5AAF2CDBD67D00B09614 /* StatefulPreviewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatefulPreviewWrapper.swift; sourceTree = "<group>"; };
440E5AB32CDCF24500B09614 /* TunnelObfuscationSettingsWatchingObservableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationSettingsWatchingObservableObject.swift; sourceTree = "<group>"; };
+ 4419AA882D282687001B13C9 /* DetailsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsContainer.swift; sourceTree = "<group>"; };
+ 4419AA8A2D2826E5001B13C9 /* DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsView.swift; sourceTree = "<group>"; };
+ 4419AA8D2D2828A4001B13C9 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
4422C0702CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscationSettingsView.swift; sourceTree = "<group>"; };
4424CDD22CDBD4A6009D8C9F /* SingleChoiceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleChoiceList.swift; sourceTree = "<group>"; };
447F3D882CDE1852006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscationSettingsViewModel.swift; sourceTree = "<group>"; };
@@ -1444,6 +1461,8 @@
4495ECD42D131A3E00A7358B /* ShadowsocksObfuscationSettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscationSettingsPage.swift; sourceTree = "<group>"; };
449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdate.swift; sourceTree = "<group>"; };
449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdateTests.swift; sourceTree = "<group>"; };
+ 449E9A6C2D283A2500F8574A /* ConnectionViewComponentPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionViewComponentPreview.swift; sourceTree = "<group>"; };
+ 449E9A6E2D283C7400F8574A /* ButtonPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonPanel.swift; sourceTree = "<group>"; };
449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceMock.swift; sourceTree = "<group>"; };
449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMock.swift; sourceTree = "<group>"; };
449EBA252B975B9700DFA4EB /* EphemeralPeerReceiving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerReceiving.swift; sourceTree = "<group>"; };
@@ -1989,6 +2008,7 @@
7AA1309E2D007B2500640DF9 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = "<group>"; };
7AA130A02D01B1E200640DF9 /* SplitMainButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitMainButton.swift; sourceTree = "<group>"; };
7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRotationTests.swift; sourceTree = "<group>"; };
+ 7AA636372D2D3BAC009B2C89 /* View+Conditionals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Conditionals.swift"; sourceTree = "<group>"; };
7AA704682C8EFE050045699D /* StoredRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredRelays.swift; sourceTree = "<group>"; };
7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListViewController.swift; sourceTree = "<group>"; };
7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListCoordinator.swift; sourceTree = "<group>"; };
@@ -2021,6 +2041,7 @@
7AF10EB32ADE85BC00C090B9 /* RelayFilterCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayFilterCoordinator.swift; sourceTree = "<group>"; };
7AF36A992CA2964000E1D497 /* AnyIPAddressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPAddressTests.swift; sourceTree = "<group>"; };
7AF6E5EF2A95051E00F2679D /* RouterBlockDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterBlockDelegate.swift; sourceTree = "<group>"; };
+ 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SelectedRelaysStub+Stubs.swift"; sourceTree = "<group>"; };
7AF9BE8A2A321BEF00DBFEDB /* RelayFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilter.swift; sourceTree = "<group>"; };
7AF9BE8D2A331C7B00DBFEDB /* RelayFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterViewModel.swift; sourceTree = "<group>"; };
7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Sorting.swift"; sourceTree = "<group>"; };
@@ -2244,7 +2265,14 @@
F0ADC3712CD3AD1600A1AD97 /* ChipCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipCollectionView.swift; sourceTree = "<group>"; };
F0ADC3732CD3C47400A1AD97 /* ChipFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFlowLayout.swift; sourceTree = "<group>"; };
F0ADF1CC2CFDFF3100299F09 /* StringConversionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringConversionError.swift; sourceTree = "<group>"; };
+ F0ADF1D02D01B55C00299F09 /* ChipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipModel.swift; sourceTree = "<group>"; };
+ F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIndicatorsViewModel.swift; sourceTree = "<group>"; };
+ F0ADF1D42D01DCFD00299F09 /* ChipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipView.swift; sourceTree = "<group>"; };
F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Async.swift"; sourceTree = "<group>"; };
+ F0B495752D02025200CFEC2A /* ChipContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipContainerView.swift; sourceTree = "<group>"; };
+ F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewModelProtocol.swift; sourceTree = "<group>"; };
+ F0B495792D02F41F00CFEC2A /* ChipFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFeature.swift; sourceTree = "<group>"; };
+ F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIndicatorsView.swift; sourceTree = "<group>"; };
F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithLocation.swift; sourceTree = "<group>"; };
F0B894F02BF751E300817A42 /* RelayWithDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithDistance.swift; sourceTree = "<group>"; };
F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelaySelector+Wireguard.swift"; sourceTree = "<group>"; };
@@ -2676,6 +2704,21 @@
path = Protocols;
sourceTree = "<group>";
};
+ 4419AA862D28264D001B13C9 /* ConnectionView */ = {
+ isa = PBXGroup;
+ children = (
+ F0ADF1CF2D01B50B00299F09 /* ChipView */,
+ 449E9A6E2D283C7400F8574A /* ButtonPanel.swift */,
+ 7AA130982CFF365A00640DF9 /* ConnectionView.swift */,
+ 449E9A6C2D283A2500F8574A /* ConnectionViewComponentPreview.swift */,
+ 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */,
+ 4419AA882D282687001B13C9 /* DetailsContainer.swift */,
+ 4419AA8A2D2826E5001B13C9 /* DetailsView.swift */,
+ 4419AA8D2D2828A4001B13C9 /* HeaderView.swift */,
+ );
+ path = ConnectionView;
+ sourceTree = "<group>";
+ };
4422C06F2CCFF6520001A385 /* Obfuscation */ = {
isa = PBXGroup;
children = (
@@ -3131,6 +3174,7 @@
7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */,
5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */,
7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */,
+ 7AA636372D2D3BAC009B2C89 /* View+Conditionals.swift */,
7A0EAE9D2D01BCBF00D3EB8B /* View+Size.swift */,
7A8A18FA2CE4B66C000BCB5B /* View+TapAreaSize.swift */,
);
@@ -3634,7 +3678,6 @@
A9D9A4C12C36D53C004088DD /* MullvadRustRuntimeTests */,
58CE5E61224146200008646E /* Products */,
584F991F2902CBDD001F858D /* Frameworks */,
- 7A0EAE982D01B29E00D3EB8B /* Recovered References */,
);
sourceTree = "<group>";
};
@@ -3935,13 +3978,6 @@
path = Edit;
sourceTree = "<group>";
};
- 7A0EAE982D01B29E00D3EB8B /* Recovered References */ = {
- isa = PBXGroup;
- children = (
- );
- name = "Recovered References";
- sourceTree = "<group>";
- };
7A2960F72A964A3500389B82 /* Alert */ = {
isa = PBXGroup;
children = (
@@ -4076,9 +4112,10 @@
7AA130972CFF364F00640DF9 /* FeatureIndicators */ = {
isa = PBXGroup;
children = (
+ 4419AA862D28264D001B13C9 /* ConnectionView */,
7AFBE3862D084C96002335FC /* ActivityIndicator.swift */,
- 7AA130982CFF365A00640DF9 /* ConnectionView.swift */,
- 7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */,
+ F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */,
+ F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */,
7AFBE3882D08915D002335FC /* FI_TunnelViewController.swift */,
);
path = FeatureIndicators;
@@ -4390,6 +4427,7 @@
F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */,
58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */,
A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */,
+ 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */,
);
path = MullvadREST;
sourceTree = "<group>";
@@ -4403,6 +4441,18 @@
path = MullvadTypes;
sourceTree = "<group>";
};
+ F0ADF1CF2D01B50B00299F09 /* ChipView */ = {
+ isa = PBXGroup;
+ children = (
+ F0B495752D02025200CFEC2A /* ChipContainerView.swift */,
+ F0B495792D02F41F00CFEC2A /* ChipFeature.swift */,
+ F0ADF1D02D01B55C00299F09 /* ChipModel.swift */,
+ F0ADF1D42D01DCFD00299F09 /* ChipView.swift */,
+ F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */,
+ );
+ path = ChipView;
+ sourceTree = "<group>";
+ };
F0DC779F2B2222D20087F09D /* Relay */ = {
isa = PBXGroup;
children = (
@@ -5881,6 +5931,7 @@
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */,
7A8A190A2CE5FFE9000BCB5B /* SettingsDAITAView.swift in Sources */,
F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */,
+ 449E9A6D2D283A2500F8574A /* ConnectionViewComponentPreview.swift in Sources */,
7A5869952B32E9C700640D27 /* LinkButton.swift in Sources */,
F09A297D2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift in Sources */,
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */,
@@ -5899,6 +5950,7 @@
F01DAE332C2B032A00521E46 /* RelaySelection.swift in Sources */,
58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */,
7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */,
+ 7AA636382D2D3BB0009B2C89 /* View+Conditionals.swift in Sources */,
5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */,
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */,
F062000C2CB7EB5D002E6DB9 /* UIImage+Helpers.swift in Sources */,
@@ -5928,6 +5980,7 @@
5878A27129091CF20096FC88 /* AccountInteractor.swift in Sources */,
7AF9BE882A30C62100DBFEDB /* SelectableSettingsCell.swift in Sources */,
58CCA010224249A1004F3011 /* TunnelViewController.swift in Sources */,
+ F0B495782D02038B00CFEC2A /* ChipViewModelProtocol.swift in Sources */,
58CEB30A2AFD584700E6E088 /* CustomCellDisclosureHandling.swift in Sources */,
58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */,
5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */,
@@ -6002,6 +6055,7 @@
58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */,
586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */,
7AF9BE902A39F26000DBFEDB /* Collection+Sorting.swift in Sources */,
+ F0B495762D02025200CFEC2A /* ChipContainerView.swift in Sources */,
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */,
7A0EAE9A2D01B41500D3EB8B /* MainButtonStyle.swift in Sources */,
58CEB3022AFD365600E6E088 /* SwitchCellContentConfiguration.swift in Sources */,
@@ -6012,6 +6066,7 @@
7A6F2FAB2AFD3097006D0856 /* CustomDNSCellFactory.swift in Sources */,
58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */,
7A6000FE2B628E9F001CF0D9 /* ListCellContentView.swift in Sources */,
+ 4419AA8B2D2826E5001B13C9 /* DetailsView.swift in Sources */,
58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */,
587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */,
7A9CCCBE2A96302800DD6A34 /* AccountDeletionCoordinator.swift in Sources */,
@@ -6075,6 +6130,7 @@
587EB66A270EFACB00123C75 /* CharacterSet+IPAddress.swift in Sources */,
5888AD83227B11080051EB06 /* LocationCell.swift in Sources */,
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */,
+ 4419AA892D282687001B13C9 /* DetailsContainer.swift in Sources */,
5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */,
7AB4CCBB2B691BBB006037F5 /* IPOverrideInteractor.swift in Sources */,
7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */,
@@ -6111,6 +6167,7 @@
588D7EDE2AF3A585005DF40A /* ListAccessMethodItem.swift in Sources */,
5827B0B02B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift in Sources */,
588D7EE02AF3A595005DF40A /* ListAccessMethodInteractor.swift in Sources */,
+ F0B4957A2D02F49200CFEC2A /* ChipFeature.swift in Sources */,
58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */,
7A8A18FD2CE4BE8D000BCB5B /* CustomToggleStyle.swift in Sources */,
58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */,
@@ -6125,6 +6182,8 @@
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */,
58FF9FE42B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift in Sources */,
+ 449E9A6F2D283C7400F8574A /* ButtonPanel.swift in Sources */,
+ 4419AA8E2D2828A4001B13C9 /* HeaderView.swift in Sources */,
5878A27329091D6D0096FC88 /* TunnelBlockObserver.swift in Sources */,
7A27E3D12CC299F90088BCFF /* VPNSettingsDetailsButtonItem.swift in Sources */,
A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */,
@@ -6145,8 +6204,10 @@
586C0D782B039CC000E7CDD7 /* AccessMethodProtocolPicker.swift in Sources */,
58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */,
7A9CCCC02A96302800DD6A34 /* ProfileVoucherCoordinator.swift in Sources */,
+ F0B4957C2D03154200CFEC2A /* FeatureIndicatorsView.swift in Sources */,
7A9CCCBC2A96302800DD6A34 /* ChangeLogCoordinator.swift in Sources */,
58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */,
+ F0ADF1D52D01DCFD00299F09 /* ChipView.swift in Sources */,
586C0D932B03D90700E7CDD7 /* ShadowsocksItemIdentifier.swift in Sources */,
58EFC7712AFB45E500E9F4CB /* SettingsChildCoordinator.swift in Sources */,
7A8A19102CEE391B000BCB5B /* RowSeparator.swift in Sources */,
@@ -6190,6 +6251,7 @@
7A9CCCC22A96302800DD6A34 /* SafariCoordinator.swift in Sources */,
58CEB3082AFD484100E6E088 /* BasicCell.swift in Sources */,
7A5869C12B57D21A00640D27 /* IPOverrideStatusView.swift in Sources */,
+ F0ADF1D32D01B6B400299F09 /* FeatureIndicatorsViewModel.swift in Sources */,
58CEB2F52AFD0BB500E6E088 /* TextCellContentConfiguration.swift in Sources */,
58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */,
F0E8CC032A4C753B007ED3B4 /* WelcomeViewController.swift in Sources */,
@@ -6209,6 +6271,7 @@
A99E5EE02B7628150033F241 /* ProblemReportViewModel.swift in Sources */,
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */,
58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */,
+ F0ADF1D12D01B55C00299F09 /* ChipModel.swift in Sources */,
F09A297B2A9F8A9B00EA3B6F /* LogoutDialogueView.swift in Sources */,
58CEB2FB2AFD13E600E6E088 /* UIListContentConfiguration+Extensions.swift in Sources */,
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */,
@@ -6494,6 +6557,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 60b0c22fe9..73d9a2d140 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/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
index d7e5dd557b..6fbb0690cb 100644
--- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
@@ -486,7 +486,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
private func makeTunnelCoordinator() -> TunnelCoordinator {
let tunnelCoordinator = TunnelCoordinator(
tunnelManager: tunnelManager,
- outgoingConnectionService: outgoingConnectionService
+ outgoingConnectionService: outgoingConnectionService,
+ ipOverrideRepository: ipOverrideRepository
)
tunnelCoordinator.showSelectLocationPicker = { [weak self] in
diff --git a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
index 42f8fba106..0c55f7e1af 100644
--- a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
@@ -6,12 +6,18 @@
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//
+import MullvadSettings
import Routing
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?
@@ -27,16 +33,22 @@ class TunnelCoordinator: Coordinator, Presenting {
init(
tunnelManager: TunnelManager,
- outgoingConnectionService: OutgoingConnectionServiceHandling
+ outgoingConnectionService: OutgoingConnectionServiceHandling,
+ ipOverrideRepository: IPOverrideRepositoryProtocol
) {
self.tunnelManager = tunnelManager
let interactor = TunnelViewControllerInteractor(
tunnelManager: tunnelManager,
- outgoingConnectionService: outgoingConnectionService
+ outgoingConnectionService: outgoingConnectionService,
+ 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 a311281940..512adaa6f7 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+Conditionals.swift b/ios/MullvadVPN/Extensions/View+Conditionals.swift
new file mode 100644
index 0000000000..39e4405da8
--- /dev/null
+++ b/ios/MullvadVPN/Extensions/View+Conditionals.swift
@@ -0,0 +1,41 @@
+//
+// View+Conditionals.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-01-07.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+extension View {
+ @ViewBuilder func `if`<Content: View>(
+ _ conditional: Bool,
+ @ViewBuilder _ content: (Self) -> Content
+ ) -> some View {
+ if conditional {
+ content(self)
+ } else {
+ self
+ }
+ }
+
+ @ViewBuilder func ifLet<Content: View, T>(
+ _ conditional: T?,
+ @ViewBuilder _ content: (Self, _ value: T) -> Content
+ ) -> some View {
+ if let value = conditional {
+ content(self, value)
+ } else {
+ self
+ }
+ }
+
+ @ViewBuilder func showIf(_ conditional: Bool) -> some View {
+ if conditional {
+ self
+ } else {
+ EmptyView()
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Extensions/View+TapAreaSize.swift b/ios/MullvadVPN/Extensions/View+TapAreaSize.swift
index 1e4ed64d37..c9ab7ee144 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
@@ -28,6 +28,5 @@ private struct TappablePadding: ViewModifier {
height: max(actualViewSize.height, tappableViewSize.height)
)
.contentShape(Rectangle())
- .frame(width: actualViewSize.width, height: actualViewSize.height)
}
}
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 ff6e723432..fc394e1bfb 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 d58fb05aa5..0000000000
--- a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/IconReload.pdf
+++ /dev/null
@@ -1,71 +0,0 @@
-%PDF-1.5
-%
-4 0 obj
-<< /Length 5 0 R
- /Filter /FlateDecode
->>
-stream
-xURKNE1wl@,p %9PxS(pI Q}1_' FEd3$Y)W kJ.^t#_S|rt'OdzW(t}.H܆"atk~kmۖyXMk7BqH)pjgH
-lBRT,hseXJ֙%P/y#\ͯ YR[<<4Ɓ(d>n3<q5M!V=)7Za
-h0ȵp? ~p{80
-i)M kdq!wma*lE!uqI A
-endstream
-endobj
-5 0 obj
- 360
-endobj
-3 0 obj
-<<
- /ExtGState <<
- /a0 << /CA 1 /ca 1 >>
- >>
->>
-endobj
-2 0 obj
-<< /Type /Page % 1
- /Parent 1 0 R
- /MediaBox [ 0 0 512 512 ]
- /Contents 4 0 R
- /Group <<
- /Type /Group
- /S /Transparency
- /I true
- /CS /DeviceRGB
- >>
- /Resources 3 0 R
->>
-endobj
-1 0 obj
-<< /Type /Pages
- /Kids [ 2 0 R ]
- /Count 1
->>
-endobj
-6 0 obj
-<< /Producer (cairo 1.16.0 (https://cairographics.org))
- /CreationDate (20200729113941+00'00)
->>
-endobj
-7 0 obj
-<< /Type /Catalog
- /Pages 1 0 R
->>
-endobj
-xref
-0 8
-0000000000 65535 f
-0000000764 00000 n
-0000000546 00000 n
-0000000474 00000 n
-0000000015 00000 n
-0000000452 00000 n
-0000000829 00000 n
-0000000943 00000 n
-trailer
-<< /Size 8
- /Root 7 0 R
- /Info 6 0 R
->>
-startxref
-995
-%%EOF
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 0000000000..6d443ac8b4
--- /dev/null
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconReload.imageset/icon-reload.svg
@@ -0,0 +1,10 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="icon">
+<mask id="mask0_774_19809" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
+<rect id="box" width="24" height="24" fill="#D9D9D9"/>
+</mask>
+<g mask="url(#mask0_774_19809)">
+<path id="vector" fill-rule="evenodd" clip-rule="evenodd" d="M6 12C6 8.68629 8.68629 6 12 6C13.7762 6 15.3729 6.77144 16.4724 8H15C14.4477 8 14 8.44772 14 9C14 9.55228 14.4477 10 15 10H19C19.5523 10 20 9.55228 20 9V5C20 4.44772 19.5523 4 19 4C18.4477 4 18 4.44772 18 5V6.70853C16.535 5.04867 14.3903 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C14.13 20 16.0674 19.1663 17.5001 17.8094C17.9011 17.4296 17.9183 16.7967 17.5386 16.3957C17.1588 15.9947 16.5259 15.9775 16.1249 16.3572C15.0487 17.3764 13.5983 18 12 18C8.68629 18 6 15.3137 6 12Z" fill="white"/>
+</g>
+</g>
+</svg>
diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift
index 1d0f98e8f0..8eeb56c159 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/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
deleted file mode 100644
index 03980fb361..0000000000
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
+++ /dev/null
@@ -1,140 +0,0 @@
-//
-// ConnectionView.swift
-// MullvadVPN
-//
-// Created by Jon Petersson on 2024-12-03.
-// Copyright © 2024 Mullvad VPN AB. All rights reserved.
-//
-
-import SwiftUI
-
-typealias ButtonAction = (ConnectionViewViewModel.TunnelControlAction) -> Void
-
-struct ConnectionView: View {
- @StateObject var viewModel: ConnectionViewViewModel
-
- var action: ButtonAction?
- var onContentUpdate: (() -> Void)?
-
- var body: some View {
- VStack(spacing: 22) {
- if viewModel.showsActivityIndicator {
- CustomProgressView(style: .large)
- }
-
- ZStack {
- BlurView(style: .dark)
-
- VStack(alignment: .leading, spacing: 16) {
- ConnectionPanel(viewModel: viewModel)
- ButtonPanel(viewModel: viewModel, action: action)
- }
- .padding(16)
- }
- .cornerRadius(12)
- .padding(16)
- }
- .onReceive(viewModel.$tunnelState, perform: { _ in
- onContentUpdate?()
- })
- .onReceive(viewModel.$showsActivityIndicator, perform: { _ in
- onContentUpdate?()
- })
- }
-}
-
-#Preview {
- ConnectionView(viewModel: ConnectionViewViewModel(tunnelState: .disconnected)) { action in
- print(action)
- }
- .background(UIColor.secondaryColor.color)
-}
-
-private struct ConnectionPanel: View {
- @StateObject var viewModel: ConnectionViewViewModel
-
- 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)
- .font(.title3.weight(.semibold))
- .foregroundStyle(UIColor.primaryTextColor.color)
- Text(server)
- .font(.body)
- .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
- }
- }
- .accessibilityLabel(viewModel.localizedAccessibilityLabel)
- }
-}
-
-private struct ButtonPanel: View {
- @StateObject var viewModel: ConnectionViewViewModel
- var action: ButtonAction?
-
- var body: some View {
- VStack(spacing: 16) {
- locationButton(with: action)
- actionButton(with: action)
- }
- }
-
- @ViewBuilder
- private func locationButton(with action: ButtonAction?) -> some View {
- switch viewModel.tunnelState {
- case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error:
- SplitMainButton(
- text: viewModel.localizedTitleForSelectLocationButton,
- image: .iconReload,
- style: .default,
- disabled: viewModel.disableButtons,
- primaryAction: { action?(.selectLocation) },
- secondaryAction: { action?(.reconnect) }
- )
- case .disconnecting, .pendingReconnect, .disconnected:
- MainButton(
- text: viewModel.localizedTitleForSelectLocationButton,
- style: .default,
- disabled: viewModel.disableButtons,
- action: { action?(.selectLocation) }
- )
- }
- }
-
- @ViewBuilder
- private func actionButton(with action: ButtonAction?) -> some View {
- switch viewModel.actionButton {
- case .connect:
- MainButton(
- text: LocalizedStringKey("Connect"),
- style: .success,
- disabled: viewModel.disableButtons,
- action: { action?(.connect) }
- )
- case .disconnect:
- MainButton(
- text: LocalizedStringKey("Disconnect"),
- style: .danger,
- disabled: viewModel.disableButtons,
- action: { action?(.disconnect) }
- )
- case .cancel:
- MainButton(
- text: LocalizedStringKey(
- viewModel.tunnelState == .waitingForConnectivity(.noConnection)
- ? "Disconnect"
- : "Cancel"
- ),
- style: .danger,
- disabled: viewModel.disableButtons,
- action: { action?(.cancel) }
- )
- }
- }
-}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift
new file mode 100644
index 0000000000..5159120046
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ButtonPanel.swift
@@ -0,0 +1,90 @@
+//
+// ButtonPanel.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2025-01-03.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+extension ConnectionView {
+ internal struct ButtonPanel: View {
+ typealias Action = (ConnectionViewViewModel.TunnelAction) -> Void
+
+ @ObservedObject var viewModel: ConnectionViewViewModel
+ var action: Action?
+
+ 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: Action?) -> some View {
+ switch viewModel.tunnelStatus.state {
+ case .connecting, .connected, .reconnecting, .waitingForConnectivity, .negotiatingEphemeralPeer, .error:
+ SplitMainButton(
+ text: viewModel.localizedTitleForSelectLocationButton,
+ image: .iconReload,
+ style: .default,
+ accessibilityId: .selectLocationButton,
+ primaryAction: { action?(.selectLocation) },
+ secondaryAction: { action?(.reconnect) }
+ )
+ case .disconnecting, .pendingReconnect, .disconnected:
+ MainButton(
+ text: viewModel.localizedTitleForSelectLocationButton,
+ style: .default,
+ action: { action?(.selectLocation) }
+ )
+ .accessibilityIdentifier(AccessibilityIdentifier.selectLocationButton.asString)
+ }
+ }
+
+ @ViewBuilder
+ private func actionButton(with action: Action?) -> some View {
+ switch viewModel.actionButton {
+ case .connect:
+ MainButton(
+ text: LocalizedStringKey("Connect"),
+ style: .success,
+ action: { action?(.connect) }
+ )
+ .accessibilityIdentifier(AccessibilityIdentifier.connectButton.asString)
+ case .disconnect:
+ MainButton(
+ text: LocalizedStringKey("Disconnect"),
+ style: .danger,
+ action: { action?(.disconnect) }
+ )
+ .accessibilityIdentifier(AccessibilityIdentifier.disconnectButton.asString)
+ case .cancel:
+ MainButton(
+ text: LocalizedStringKey(
+ viewModel.tunnelStatus.state == .waitingForConnectivity(.noConnection)
+ ? "Disconnect"
+ : "Cancel"
+ ),
+ style: .danger,
+ action: { action?(.cancel) }
+ )
+ .accessibilityIdentifier(
+ viewModel.tunnelStatus.state == .waitingForConnectivity(.noConnection)
+ ? AccessibilityIdentifier.disconnectButton.asString
+ : AccessibilityIdentifier.cancelButton.asString
+ )
+ }
+ }
+ }
+}
+
+#Preview {
+ ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { _, vm, _ in
+ ConnectionView.ButtonPanel(viewModel: vm, action: nil)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipContainerView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipContainerView.swift
new file mode 100644
index 0000000000..0d417542f3
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipContainerView.swift
@@ -0,0 +1,95 @@
+//
+// ChipContainerView.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-12-05.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol {
+ @ObservedObject var viewModel: ViewModel
+ @Binding var isExpanded: Bool
+
+ @State private var chipContainerHeight: CGFloat = .zero
+ private let verticalPadding: CGFloat = 6
+
+ var body: some View {
+ GeometryReader { geo in
+ let containerWidth = geo.size.width
+
+ let (chipsToAdd, showMoreButton) = if isExpanded {
+ (viewModel.chips, false)
+ } else {
+ viewModel.chipsToAdd(forContainerWidth: containerWidth)
+ }
+
+ HStack {
+ ZStack(alignment: .topLeading) {
+ createChipViews(chips: chipsToAdd, containerWidth: containerWidth)
+ }
+
+ Button(LocalizedStringKey("\(viewModel.chips.count - chipsToAdd.count) more...")) {
+ isExpanded.toggle()
+ }
+ .font(.subheadline)
+ .lineLimit(1)
+ .foregroundStyle(UIColor.primaryTextColor.color)
+ .showIf(showMoreButton)
+
+ Spacer()
+ }
+ .sizeOfView { chipContainerHeight = $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 {
+ var width = CGFloat.zero
+ var height = CGFloat.zero
+
+ return ForEach(chips) { data in
+ ChipView(item: data)
+ .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 {
+ width = 0
+ } else {
+ width -= dimension.width
+ }
+ return result
+ }
+ .alignmentGuide(.top) { _ in
+ let result = height
+ if data.id == chips.last?.id {
+ height = 0
+ }
+ return result
+ }
+ }
+ }
+}
+
+#Preview("Tap to expand") {
+ StatefulPreviewWrapper(false) { isExpanded in
+ ChipContainerView(
+ viewModel: MockFeatureIndicatorsViewModel(),
+ isExpanded: isExpanded
+ )
+ .background(UIColor.secondaryColor.color)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipFeature.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipFeature.swift
new file mode 100644
index 0000000000..a661a2cefc
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipFeature.swift
@@ -0,0 +1,127 @@
+//
+// ChipFeature.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-12-06.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+import MullvadSettings
+import SwiftUI
+
+// Opting to use NSLocalizedString instead of LocalizedStringKey here in order
+// to be able to fetch the string value at a later point (eg. in ChipViewModelProtocol,
+// when calculating the text widths of the chips).
+
+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/ConnectionView/ChipView/ChipModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipModel.swift
new file mode 100644
index 0000000000..a746897c06
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipModel.swift
@@ -0,0 +1,15 @@
+//
+// FeatureChipModel.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-12-05.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import SwiftUI
+
+struct ChipModel: Identifiable {
+ let id = UUID()
+ let name: String
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipView.swift
new file mode 100644
index 0000000000..57fc7cb042
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipView.swift
@@ -0,0 +1,40 @@
+//
+// FeatureChipView.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-12-05.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct ChipView: View {
+ let item: ChipModel
+ var body: some View {
+ Text(item.name)
+ .font(.subheadline)
+ .lineLimit(1)
+ .foregroundStyle(UIColor.primaryTextColor.color)
+ .padding(.horizontal, UIMetrics.FeatureIndicators.chipViewHorisontalPadding)
+ .padding(.vertical, 4)
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .stroke(
+ UIColor.primaryColor.color,
+ lineWidth: 1
+ )
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(UIColor.secondaryColor.color)
+ )
+ )
+ }
+}
+
+#Preview {
+ ZStack {
+ ChipView(item: ChipModel(name: "Example"))
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(UIColor.secondaryColor.color)
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipViewModelProtocol.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipViewModelProtocol.swift
new file mode 100644
index 0000000000..3a4c9da337
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ChipView/ChipViewModelProtocol.swift
@@ -0,0 +1,65 @@
+//
+// ChipViewModelProtocol.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-12-05.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+protocol ChipViewModelProtocol: ObservableObject {
+ var chips: [ChipModel] { get }
+}
+
+extension ChipViewModelProtocol {
+ func chipsToAdd(forContainerWidth containerWidth: CGFloat) -> (chips: [ChipModel], isOverflowing: Bool) {
+ var chipsToAdd = [ChipModel]()
+ var isOverflowing = false
+
+ let moreTextWidth = String(
+ format: NSLocalizedString(
+ "CONNECTION_VIEW_CHIPS_MORE",
+ tableName: "ConnectionView",
+ value: "@d more...",
+ comment: ""
+ ), arguments: [chips.count]
+ )
+ .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
+
+ let chipWillFitWithMoreText = (totalChipsWidth + moreTextWidth) <= containerWidth
+ let chipWillFit = totalChipsWidth <= containerWidth
+
+ guard (chipWillFit && isLastChip) || chipWillFitWithMoreText else {
+ isOverflowing = true
+ break
+ }
+
+ chipsToAdd.append(chip)
+ }
+
+ return (chipsToAdd, isOverflowing)
+ }
+}
+
+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/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift
new file mode 100644
index 0000000000..6dfee3abed
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionView.swift
@@ -0,0 +1,89 @@
+//
+// ConnectionView.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-12-03.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct ConnectionView: View {
+ @ObservedObject var connectionViewModel: ConnectionViewViewModel
+ @ObservedObject var indicatorsViewModel: FeatureIndicatorsViewModel
+
+ @State private(set) var isExpanded = false
+
+ var action: ButtonPanel.Action?
+ var onContentUpdate: (() -> Void)?
+
+ var body: some View {
+ Spacer()
+ .accessibilityIdentifier(AccessibilityIdentifier.connectionView.asString)
+
+ VStack(spacing: 22) {
+ CustomProgressView(style: .large)
+ .showIf(connectionViewModel.showsActivityIndicator)
+
+ ZStack {
+ BlurView(style: .dark)
+
+ VStack(alignment: .leading, spacing: 0) {
+ HeaderView(viewModel: connectionViewModel, isExpanded: $isExpanded)
+ .padding(.bottom, headerViewBottomPadding)
+
+ DetailsContainer(
+ connectionViewModel: connectionViewModel,
+ indicatorsViewModel: indicatorsViewModel,
+ isExpanded: $isExpanded
+ )
+ .showIf(connectionViewModel.showConnectionDetails)
+
+ ButtonPanel(viewModel: connectionViewModel, action: action)
+ .padding(.top, 16)
+ }
+ .padding(16)
+ }
+ .cornerRadius(12)
+ .padding(16)
+ }
+ .padding(.bottom, 8) // Some spacing to avoid overlap with the map legal link.
+ .onChange(of: isExpanded) { _ in
+ onContentUpdate?()
+ }
+ .onReceive(connectionViewModel.combinedState) { _, _ in
+ // Only update expanded state when connections details should be hidden.
+ // This will contract the view on eg. disconnect, but leave it as-is on
+ // eg. connect.
+ if !connectionViewModel.showConnectionDetails {
+ isExpanded = false
+ return
+ }
+
+ onContentUpdate?()
+ }
+ }
+}
+
+extension ConnectionView {
+ var headerViewBottomPadding: CGFloat {
+ let hasIndicators = !indicatorsViewModel.chips.isEmpty
+ let showConnectionDetails = connectionViewModel.showConnectionDetails
+
+ return isExpanded
+ ? 16
+ : hasIndicators && showConnectionDetails ? 16 : 0
+ }
+}
+
+#Preview("ConnectionView (Indicators)") {
+ ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { indicatorModel, viewModel, _ in
+ ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel)
+ }
+}
+
+#Preview("ConnectionView (No indicators)") {
+ ConnectionViewComponentPreview(showIndicators: false, isExpanded: true) { indicatorModel, viewModel, _ in
+ ConnectionView(connectionViewModel: viewModel, indicatorsViewModel: indicatorModel)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewComponentPreview.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewComponentPreview.swift
new file mode 100644
index 0000000000..cc24537f13
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewComponentPreview.swift
@@ -0,0 +1,68 @@
+//
+// ConnectionViewComponentPreview.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2025-01-03.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadMockData
+import MullvadSettings
+import MullvadTypes
+import PacketTunnelCore
+import SwiftUI
+
+struct ConnectionViewComponentPreview<Content: View>: View {
+ let showIndicators: Bool
+
+ private var tunnelSettings: LatestTunnelSettings {
+ LatestTunnelSettings(
+ wireGuardObfuscation: WireGuardObfuscationSettings(state: showIndicators ? .udpOverTcp : .off),
+ tunnelQuantumResistance: showIndicators ? .on : .off,
+ tunnelMultihopState: showIndicators ? .on : .off,
+ daita: DAITASettings(daitaState: showIndicators ? .on : .off)
+ )
+ }
+
+ 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)
+ )
+ )
+
+ var content: (FeatureIndicatorsViewModel, ConnectionViewViewModel, Binding<Bool>) -> Content
+
+ @State var isExpanded: Bool
+
+ init(
+ showIndicators: Bool,
+ isExpanded: Bool,
+ content: @escaping (FeatureIndicatorsViewModel, ConnectionViewViewModel, Binding<Bool>) -> Content
+ ) {
+ self.showIndicators = showIndicators
+ self._isExpanded = State(wrappedValue: isExpanded)
+ self.content = content
+ }
+
+ var body: some View {
+ content(
+ FeatureIndicatorsViewModel(
+ tunnelSettings: tunnelSettings,
+ ipOverrides: []
+ ),
+ viewModel,
+ $isExpanded
+ )
+ .background(UIColor.secondaryColor.color)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/ConnectionViewViewModel.swift
index 29a4748b41..962eaa0d63 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionViewViewModel.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/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<TunnelStatus>.Publisher,
+ Published<Bool>.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:
+ true
+ case .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork), .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,18 @@ extension ConnectionViewViewModel {
}
}
- var actionButton: TunnelControlActionButton {
- switch tunnelState {
+ var localizedTitleForSelectLocationButton: LocalizedStringKey {
+ switch tunnelStatus.state {
+ case .disconnecting, .pendingReconnect, .disconnected, .waitingForConnectivity(.noNetwork):
+ LocalizedStringKey("Select location")
+ case .connecting, .connected, .reconnecting, .waitingForConnectivity(.noConnection),
+ .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 +151,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 +159,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 +172,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/ConnectionView/DetailsContainer.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift
new file mode 100644
index 0000000000..6b2bb00399
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsContainer.swift
@@ -0,0 +1,57 @@
+//
+// DetailsContainer.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2025-01-03.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+extension ConnectionView {
+ internal struct DetailsContainer: View {
+ @ObservedObject var connectionViewModel: ConnectionViewViewModel
+ @ObservedObject var indicatorsViewModel: FeatureIndicatorsViewModel
+ @Binding var isExpanded: Bool
+
+ @State private var scrollViewHeight: CGFloat = 0
+
+ var body: some View {
+ VStack(spacing: 16) {
+ Divider()
+ .background(UIColor.secondaryTextColor.color)
+ .showIf(isExpanded)
+
+ ScrollView {
+ VStack(spacing: 16) {
+ FeatureIndicatorsView(
+ viewModel: indicatorsViewModel,
+ isExpanded: $isExpanded
+ )
+ .showIf(!indicatorsViewModel.chips.isEmpty)
+
+ DetailsView(viewModel: connectionViewModel)
+ .showIf(isExpanded)
+ }
+ .sizeOfView { scrollViewHeight = $0.height }
+ }
+ .frame(maxHeight: scrollViewHeight)
+ .onTapGesture {
+ // If this callback is not set the child views will not reliably register tap events.
+ // This is a bug in iOS 16 and 17, but seemingly fixed in 18. Once we set the lowest
+ // supported version to iOS 18 we can probably remove it.
+ }
+ }
+ }
+ }
+}
+
+#Preview {
+ ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { indicatorModel, viewModel, isExpanded in
+ ConnectionView.DetailsContainer(
+ connectionViewModel: viewModel,
+ indicatorsViewModel: indicatorModel,
+ isExpanded: isExpanded
+ )
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift
new file mode 100644
index 0000000000..ff07dc94b5
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/DetailsView.swift
@@ -0,0 +1,64 @@
+//
+// DetailsView.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2025-01-03.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+extension ConnectionView {
+ internal struct DetailsView: View {
+ @ObservedObject 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)
+ }
+ }
+ }
+ }
+ }
+
+ @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)
+ }
+ }
+ }
+}
+
+#Preview {
+ ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { _, vm, _ in
+ ConnectionView.DetailsView(viewModel: vm)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift
new file mode 100644
index 0000000000..fa1112a80e
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView/HeaderView.swift
@@ -0,0 +1,63 @@
+//
+// HeaderView.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2025-01-03.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+extension ConnectionView {
+ internal struct HeaderView: View {
+ @ObservedObject var viewModel: ConnectionViewViewModel
+ @Binding var isExpanded: Bool
+
+ var body: some View {
+ HStack(alignment: .top) {
+ VStack(alignment: .leading, spacing: 0) {
+ Text(viewModel.localizedTitleForSecureLabel)
+ .textCase(.uppercase)
+ .font(.title3.weight(.semibold))
+ .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)
+
+ Group {
+ Spacer()
+ Image(.iconChevron)
+ .renderingMode(.template)
+ .rotationEffect(isExpanded ? .degrees(-90) : .degrees(90))
+ .foregroundStyle(.white)
+ .accessibilityIdentifier(AccessibilityIdentifier.relayStatusCollapseButton.asString)
+ }
+ .showIf(viewModel.showConnectionDetails)
+ }
+ .contentShape(Rectangle())
+ .onTapGesture {
+ isExpanded.toggle()
+ }
+ }
+ }
+}
+
+#Preview {
+ ConnectionViewComponentPreview(showIndicators: true, isExpanded: true) { _, vm, isExpanded in
+ ConnectionView.HeaderView(viewModel: vm, isExpanded: isExpanded)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
index b70c3a9ffa..bb754a0c25 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
@@ -6,8 +6,10 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
+import Combine
import MapKit
import MullvadLogging
+import MullvadSettings
import MullvadTypes
import SwiftUI
@@ -17,7 +19,8 @@ class FI_TunnelViewController: UIViewController, RootContainment {
private let logger = Logger(label: "TunnelViewController")
private let interactor: TunnelViewControllerInteractor
private var tunnelState: TunnelState = .disconnected
- private var viewModel = ConnectionViewViewModel(tunnelState: .disconnected)
+ private var connectionViewViewModel: ConnectionViewViewModel
+ private var indicatorsViewViewModel: FeatureIndicatorsViewModel
private var connectionView: ConnectionView
private var connectionController: UIHostingController<ConnectionView>?
@@ -48,7 +51,18 @@ class FI_TunnelViewController: UIViewController, RootContainment {
init(interactor: TunnelViewControllerInteractor) {
self.interactor = interactor
- connectionView = ConnectionView(viewModel: self.viewModel)
+
+ tunnelState = interactor.tunnelStatus.state
+ connectionViewViewModel = ConnectionViewViewModel(tunnelStatus: interactor.tunnelStatus)
+ indicatorsViewViewModel = FeatureIndicatorsViewModel(
+ tunnelSettings: interactor.tunnelSettings,
+ ipOverrides: interactor.ipOverrides
+ )
+
+ connectionView = ConnectionView(
+ connectionViewModel: self.connectionViewViewModel,
+ indicatorsViewModel: self.indicatorsViewViewModel
+ )
super.init(nibName: nil, bundle: nil)
@@ -72,11 +86,23 @@ class FI_TunnelViewController: UIViewController, RootContainment {
}
interactor.didUpdateTunnelStatus = { [weak self] tunnelStatus in
+ self?.connectionViewViewModel.tunnelStatus = tunnelStatus
self?.setTunnelState(tunnelStatus.state, animated: true)
- self?.viewModel.tunnelState = tunnelStatus.state
self?.view.setNeedsLayout()
}
+ interactor.didGetOutgoingAddress = { [weak self] connectionInfo in
+ self?.connectionViewViewModel.outgoingConnectionInfo = connectionInfo
+ }
+
+ interactor.didUpdateTunnelSettings = { [weak self] tunnelSettings in
+ self?.indicatorsViewViewModel.tunnelSettings = tunnelSettings
+ }
+
+ interactor.didUpdateIpOverrides = { [weak self] overrides in
+ self?.indicatorsViewViewModel.ipOverrides = overrides
+ }
+
connectionView.action = { [weak self] action in
switch action {
case .connect:
@@ -102,10 +128,6 @@ class FI_TunnelViewController: UIViewController, RootContainment {
addMapController()
addContentView()
-
- tunnelState = interactor.tunnelStatus.state
- viewModel.tunnelState = tunnelState
-
updateMap(animated: false)
}
@@ -125,6 +147,7 @@ class FI_TunnelViewController: UIViewController, RootContainment {
private func setTunnelState(_ tunnelState: TunnelState, animated: Bool) {
self.tunnelState = tunnelState
+
setNeedsHeaderBarStyleAppearanceUpdate()
guard isViewLoaded else { return }
@@ -137,17 +160,17 @@ class FI_TunnelViewController: UIViewController, RootContainment {
case let .connecting(tunnelRelays, _, _):
mapViewController.removeLocationMarker()
mapViewController.setCenter(tunnelRelays?.exit.location.geoCoordinate, animated: animated)
- viewModel.showsActivityIndicator = true
+ connectionViewViewModel.showsActivityIndicator = true
case let .reconnecting(tunnelRelays, _, _), let .negotiatingEphemeralPeer(tunnelRelays, _, _, _):
mapViewController.removeLocationMarker()
mapViewController.setCenter(tunnelRelays.exit.location.geoCoordinate, animated: animated)
- viewModel.showsActivityIndicator = true
+ connectionViewViewModel.showsActivityIndicator = true
case let .connected(tunnelRelays, _, _):
let center = tunnelRelays.exit.location.geoCoordinate
mapViewController.setCenter(center, animated: animated) {
- self.viewModel.showsActivityIndicator = false
+ self.connectionViewViewModel.showsActivityIndicator = false
// Connection can change during animation, so make sure we're still connected before adding marker.
if case .connected = self.tunnelState {
@@ -157,16 +180,16 @@ class FI_TunnelViewController: UIViewController, RootContainment {
case .pendingReconnect:
mapViewController.removeLocationMarker()
- viewModel.showsActivityIndicator = true
+ connectionViewViewModel.showsActivityIndicator = true
case .waitingForConnectivity, .error:
mapViewController.removeLocationMarker()
- viewModel.showsActivityIndicator = false
+ connectionViewViewModel.showsActivityIndicator = false
case .disconnected, .disconnecting:
mapViewController.removeLocationMarker()
mapViewController.setCenter(nil, animated: animated)
- viewModel.showsActivityIndicator = false
+ connectionViewViewModel.showsActivityIndicator = false
}
}
@@ -192,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
new file mode 100644
index 0000000000..70e49a9c04
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift
@@ -0,0 +1,33 @@
+//
+// FeaturesIndicatorsView.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-12-06.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct FeatureIndicatorsView<ViewModel>: View where ViewModel: ChipViewModelProtocol {
+ @ObservedObject var viewModel: ViewModel
+ @Binding var isExpanded: Bool
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text(LocalizedStringKey("Active features"))
+ .font(.footnote.weight(.semibold))
+ .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
+ .showIf(isExpanded)
+
+ ChipContainerView(viewModel: viewModel, isExpanded: $isExpanded)
+ }
+ }
+}
+
+#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
new file mode 100644
index 0000000000..97eac59ca8
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift
@@ -0,0 +1,35 @@
+//
+// FeatureIndicatorsViewModel.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-12-05.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadSettings
+import SwiftUI
+
+class FeatureIndicatorsViewModel: ChipViewModelProtocol {
+ @Published var tunnelSettings: LatestTunnelSettings
+ @Published var ipOverrides: [IPOverride]
+
+ init(tunnelSettings: LatestTunnelSettings, ipOverrides: [IPOverride]) {
+ self.tunnelSettings = tunnelSettings
+ self.ipOverrides = ipOverrides
+ }
+
+ var chips: [ChipModel] {
+ let features: [ChipFeature] = [
+ DaitaFeature(settings: tunnelSettings),
+ QuantumResistanceFeature(settings: tunnelSettings),
+ MultihopFeature(settings: tunnelSettings),
+ ObfuscationFeature(settings: tunnelSettings),
+ DNSFeature(settings: tunnelSettings),
+ IPOverrideFeature(overrides: ipOverrides),
+ ]
+
+ return features
+ .filter { $0.isEnabled }
+ .map { ChipModel(name: $0.name) }
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
index 88c933493b..cff5222976 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/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
index 7cf879f3bf..78bd6c27b0 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
@@ -65,7 +65,7 @@ class TunnelViewController: UIViewController, RootContainment {
self?.updateViewModel(tunnelStatus: tunnelStatus)
}
- interactor.didGetOutGoingAddress = { [weak self] outgoingConnectionInfo in
+ interactor.didGetOutgoingAddress = { [weak self] outgoingConnectionInfo in
self?.updateViewModel(outgoingConnectionInfo: outgoingConnectionInfo)
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift
index 47b75fd7d5..e072ec283e 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift
@@ -6,7 +6,7 @@
// Copyright © 2022 Mullvad VPN AB. All rights reserved.
//
-import Foundation
+import Combine
import MullvadSettings
import MullvadTypes
@@ -15,10 +15,14 @@ final class TunnelViewControllerInteractor {
private let outgoingConnectionService: OutgoingConnectionServiceHandling
private var tunnelObserver: TunnelObserver?
private var outgoingConnectionTask: Task<Void, Error>?
+ private var ipOverrideRepository: IPOverrideRepositoryProtocol
+ private var cancellables: Set<Combine.AnyCancellable> = []
var didUpdateTunnelStatus: ((TunnelStatus) -> Void)?
var didUpdateDeviceState: ((_ deviceState: DeviceState, _ previousDeviceState: DeviceState) -> Void)?
- var didGetOutGoingAddress: (@MainActor (OutgoingConnectionInfo) -> Void)?
+ var didUpdateTunnelSettings: ((LatestTunnelSettings) -> Void)?
+ var didUpdateIpOverrides: (([IPOverride]) -> Void)?
+ var didGetOutgoingAddress: (@MainActor (OutgoingConnectionInfo) -> Void)?
var tunnelStatus: TunnelStatus {
tunnelManager.tunnelStatus
@@ -28,16 +32,26 @@ final class TunnelViewControllerInteractor {
tunnelManager.deviceState
}
+ var tunnelSettings: LatestTunnelSettings {
+ tunnelManager.settings
+ }
+
+ var ipOverrides: [IPOverride] {
+ ipOverrideRepository.fetchAll()
+ }
+
deinit {
outgoingConnectionTask?.cancel()
}
init(
tunnelManager: TunnelManager,
- outgoingConnectionService: OutgoingConnectionServiceHandling
+ outgoingConnectionService: OutgoingConnectionServiceHandling,
+ ipOverrideRepository: IPOverrideRepositoryProtocol
) {
self.tunnelManager = tunnelManager
self.outgoingConnectionService = outgoingConnectionService
+ self.ipOverrideRepository = ipOverrideRepository
let tunnelObserver = TunnelBlockObserver(
didUpdateTunnelStatus: { [weak self] _, tunnelStatus in
@@ -50,18 +64,27 @@ final class TunnelViewControllerInteractor {
.getOutgoingConnectionInfo() else {
return
}
- await self?.didGetOutGoingAddress?(outgoingConnectionInfo)
+ await self?.didGetOutgoingAddress?(outgoingConnectionInfo)
}
}
},
didUpdateDeviceState: { [weak self] _, deviceState, previousDeviceState in
self?.didUpdateDeviceState?(deviceState, previousDeviceState)
+ },
+ didUpdateTunnelSettings: { [weak self] _, tunnelSettings in
+ self?.didUpdateTunnelSettings?(tunnelSettings)
}
)
tunnelManager.addObserver(tunnelObserver)
self.tunnelObserver = tunnelObserver
+
+ ipOverrideRepository.overridesPublisher
+ .sink { [weak self] overrides in
+ self?.didUpdateIpOverrides?(overrides)
+ }
+ .store(in: &cancellables)
}
func startTunnel() {
diff --git a/ios/MullvadVPN/Views/MainButton.swift b/ios/MullvadVPN/Views/MainButton.swift
index 679b34a2cd..a4240c433d 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 f638c87ac2..e13758a155 100644
--- a/ios/MullvadVPN/Views/MainButtonStyle.swift
+++ b/ios/MullvadVPN/Views/MainButtonStyle.swift
@@ -10,28 +10,26 @@ import SwiftUI
struct MainButtonStyle: ButtonStyle {
var style: Style
- @State var disabled: Bool
+ @Environment(\.isEnabled) private var isEnabled: Bool
- init(_ style: Style, disabled: Bool = false) {
+ init(_ style: Style) {
self.style = style
- self.disabled = disabled
}
func makeBody(configuration: Configuration) -> some View {
- configuration.label
- .padding(.horizontal, 8)
+ return configuration.label
.frame(height: 44)
.foregroundColor(
- configuration.isPressed
- ? UIColor.secondaryTextColor.color
- : disabled
- ? UIColor.primaryTextColor.withAlphaComponent(0.2).color
- : UIColor.primaryTextColor.color
+ isEnabled
+ ? UIColor.primaryTextColor.color
+ : UIColor.primaryTextColor.withAlphaComponent(0.2).color
)
.background(
- disabled
- ? style.color.darkened(by: 0.6)
- : style.color
+ isEnabled
+ ? configuration.isPressed
+ ? style.pressedColor
+ : style.color
+ : style.disabledColor
)
.font(.body.weight(.semibold))
}
@@ -46,12 +44,20 @@ 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
}
}
+
+ var pressedColor: Color {
+ color.darkened(by: 0.4)!
+ }
+
+ var disabledColor: Color {
+ color.darkened(by: 0.6)!
+ }
}
}
diff --git a/ios/MullvadVPN/Views/SplitMainButton.swift b/ios/MullvadVPN/Views/SplitMainButton.swift
index 11336f424b..72a8798935 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
+ var accessibilityId: AccessibilityIdentifier?
+
+ @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 +27,23 @@ struct SplitMainButton: View {
Text(text)
Spacer()
}
- .padding(.trailing, -width)
+ .padding(.trailing, -secondaryButtonWidth)
})
+ .ifLet(accessibilityId) { view, value in
+ view.accessibilityIdentifier(value.asString)
+ }
+
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/MullvadVPNTests/MullvadSettings/IPOverrideRepositoryStub.swift b/ios/MullvadVPNTests/MullvadSettings/IPOverrideRepositoryStub.swift
index 633bc44bdb..c27c5cff58 100644
--- a/ios/MullvadVPNTests/MullvadSettings/IPOverrideRepositoryStub.swift
+++ b/ios/MullvadVPNTests/MullvadSettings/IPOverrideRepositoryStub.swift
@@ -6,9 +6,15 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
+import Combine
import MullvadSettings
struct IPOverrideRepositoryStub: IPOverrideRepositoryProtocol {
+ let passthroughSubject: CurrentValueSubject<[IPOverride], Never> = CurrentValueSubject([])
+ var overridesPublisher: AnyPublisher<[IPOverride], Never> {
+ passthroughSubject.eraseToAnyPublisher()
+ }
+
let overrides: [IPOverride]
init(overrides: [IPOverride] = []) {
diff --git a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift
index 6929ee1f5a..da47a2c334 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 469bf0fa20..9fbc650d70 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 5419e6282d..637068cfe6 100755
--- a/ios/convert-assets.rb
+++ b/ios/convert-assets.rb
@@ -32,7 +32,6 @@ GRAPHICAL_ASSETS = [
"icon-extLink.svg",
"icon-fail.svg",
"icon-info.svg",
- "icon-reload.svg",
"icon-settings.svg",
"icon-spinner.svg",
"icon-success.svg",