summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authormojganii <mojgan.jelodar@codic.se>2024-12-11 15:13:09 +0100
committerJon Petersson <jon.petersson@mullvad.net>2025-01-13 09:34:43 +0100
commita330c48b39db414b38332f5dd45de77337d53a89 (patch)
tree34a784965804d4cd1de73db91e16adb9051eb84c /ios
parentcde728046d456248549f11a790a3a4916848bb18 (diff)
downloadmullvadvpn-a330c48b39db414b38332f5dd45de77337d53a89.tar.xz
mullvadvpn-a330c48b39db414b38332f5dd45de77337d53a89.zip
Add FeatureIndicatorsView
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadSettings/IPOverrideRepository.swift10
-rw-r--r--ios/MullvadSettings/WireGuardObfuscationSettings.swift4
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj36
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift3
-rw-r--r--ios/MullvadVPN/Coordinators/TunnelCoordinator.swift7
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift88
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift95
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift15
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift40
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift32
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift26
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift45
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift31
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift37
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift27
-rw-r--r--ios/MullvadVPN/Views/MainButtonStyle.swift22
-rw-r--r--ios/MullvadVPNTests/MullvadSettings/IPOverrideRepositoryStub.swift6
17 files changed, 492 insertions, 32 deletions
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..95ae9dc0ec 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -1001,7 +1001,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 /* ChipFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B495792D02F41F00CFEC2A /* ChipFeatures.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 */; };
@@ -2244,7 +2251,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 /* ChipFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFeatures.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>"; };
@@ -4076,9 +4090,13 @@
7AA130972CFF364F00640DF9 /* FeatureIndicators */ = {
isa = PBXGroup;
children = (
+ F0ADF1CF2D01B50B00299F09 /* ChipView */,
7AFBE3862D084C96002335FC /* ActivityIndicator.swift */,
+ F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */,
7AA130982CFF365A00640DF9 /* ConnectionView.swift */,
7A0EAEA32D06DF8200D3EB8B /* ConnectionViewViewModel.swift */,
+ F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */,
+ F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */,
7AFBE3882D08915D002335FC /* FI_TunnelViewController.swift */,
);
path = FeatureIndicators;
@@ -4403,6 +4421,17 @@
path = MullvadTypes;
sourceTree = "<group>";
};
+ F0ADF1CF2D01B50B00299F09 /* ChipView */ = {
+ isa = PBXGroup;
+ children = (
+ F0B495752D02025200CFEC2A /* ChipContainerView.swift */,
+ F0ADF1D02D01B55C00299F09 /* ChipModel.swift */,
+ F0ADF1D42D01DCFD00299F09 /* ChipView.swift */,
+ F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */,
+ );
+ path = ChipView;
+ sourceTree = "<group>";
+ };
F0DC779F2B2222D20087F09D /* Relay */ = {
isa = PBXGroup;
children = (
@@ -5928,6 +5957,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 +6032,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 */,
@@ -6111,6 +6142,7 @@
588D7EDE2AF3A585005DF40A /* ListAccessMethodItem.swift in Sources */,
5827B0B02B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift in Sources */,
588D7EE02AF3A595005DF40A /* ListAccessMethodInteractor.swift in Sources */,
+ F0B4957A2D02F49200CFEC2A /* ChipFeatures.swift in Sources */,
58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */,
7A8A18FD2CE4BE8D000BCB5B /* CustomToggleStyle.swift in Sources */,
58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */,
@@ -6145,8 +6177,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 +6224,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 +6244,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 */,
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..7a8145ddca 100644
--- a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift
@@ -6,6 +6,7 @@
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//
+import MullvadSettings
import Routing
import UIKit
@@ -27,13 +28,15 @@ 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
)
controller = TunnelViewController(interactor: interactor)
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift
new file mode 100644
index 0000000000..c005b3f080
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift
@@ -0,0 +1,88 @@
+//
+// ChipFeatures.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-12-06.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+import Foundation
+import MullvadSettings
+import SwiftUI
+
+protocol ChipFeature {
+ var isEnabled: Bool { get }
+ var name: LocalizedStringKey { get }
+}
+
+struct DaitaFeature: ChipFeature {
+ let settings: LatestTunnelSettings
+
+ var isEnabled: Bool {
+ settings.daita.daitaState.isEnabled
+ }
+
+ var name: LocalizedStringKey {
+ LocalizedStringKey("DAITA")
+ }
+}
+
+struct QuantumResistanceFeature: ChipFeature {
+ let settings: LatestTunnelSettings
+ var isEnabled: Bool {
+ settings.tunnelQuantumResistance.isEnabled
+ }
+
+ var name: LocalizedStringKey {
+ LocalizedStringKey("Quantum resistance")
+ }
+}
+
+struct MultihopFeature: ChipFeature {
+ let settings: LatestTunnelSettings
+ var isEnabled: Bool {
+ settings.tunnelMultihopState.isEnabled
+ }
+
+ var name: LocalizedStringKey {
+ LocalizedStringKey("Multihop")
+ }
+}
+
+struct ObfuscationFeature: ChipFeature {
+ let settings: LatestTunnelSettings
+
+ var isEnabled: Bool {
+ settings.wireGuardObfuscation.state.isEnabled
+ }
+
+ var name: LocalizedStringKey {
+ LocalizedStringKey("Obfuscation")
+ }
+}
+
+struct DNSFeature: ChipFeature {
+ let settings: LatestTunnelSettings
+
+ var isEnabled: Bool {
+ settings.dnsSettings.enableCustomDNS || !settings.dnsSettings.blockingOptions.isEmpty
+ }
+
+ var name: LocalizedStringKey {
+ if !settings.dnsSettings.blockingOptions.isEmpty {
+ return LocalizedStringKey("DNS content blockers")
+ }
+ return LocalizedStringKey("Custom DNS")
+ }
+}
+
+struct IPOverrideFeature: ChipFeature {
+ let overrides: [IPOverride]
+
+ var isEnabled: Bool {
+ !overrides.isEmpty
+ }
+
+ var name: LocalizedStringKey {
+ LocalizedStringKey("Server IP override")
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift
new file mode 100644
index 0000000000..f06124ed48
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/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
+
+ @State var chipHeight: CGFloat = 0
+ @State var fullContainerHeight: CGFloat = 0
+ @State var visibleContainerHeight: CGFloat = 0
+
+ var body: some View {
+ GeometryReader { geo in
+ let containerWidth = geo.size.width
+ let chipsOverflow = !viewModel.isExpanded && (fullContainerHeight > chipHeight)
+ let numberOfChips = chipsOverflow ? 2 : viewModel.chips.count
+
+ HStack {
+ ZStack(alignment: .topLeading) {
+ createChipViews(chips: Array(viewModel.chips.prefix(numberOfChips)), containerWidth: containerWidth)
+ }
+ .sizeOfView { visibleContainerHeight = $0.height }
+
+ if chipsOverflow {
+ Text(LocalizedStringKey("\(viewModel.chips.count - numberOfChips) more..."))
+ .font(.subheadline)
+ .lineLimit(1)
+ .foregroundStyle(UIColor.primaryTextColor.color)
+ .padding(.bottom, 12)
+ }
+
+ Spacer()
+ }
+ .background(preRenderViewSize(containerWidth: containerWidth))
+ }.frame(height: visibleContainerHeight)
+ }
+
+ // Renders all chips on screen, in this case specifically to get their combined height.
+ // Used to determine if content would overflow if view was not expanded and should
+ // only be called from a background modifier.
+ private func preRenderViewSize(containerWidth: CGFloat) -> some View {
+ ZStack(alignment: .topLeading) {
+ createChipViews(chips: viewModel.chips, containerWidth: containerWidth)
+ }
+ .hidden()
+ .sizeOfView { fullContainerHeight = $0.height }
+ }
+
+ 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: 6, leading: 0, bottom: 6, trailing: 8))
+ .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
+ }
+ .sizeOfView { chipHeight = $0.height }
+ }
+ }
+}
+
+#Preview("Normal") {
+ ChipContainerView(viewModel: MockFeatureIndicatorsViewModel())
+ .background(UIColor.secondaryColor.color)
+}
+
+#Preview("Expanded") {
+ ChipContainerView(viewModel: MockFeatureIndicatorsViewModel(isExpanded: true))
+ .background(UIColor.secondaryColor.color)
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift
new file mode 100644
index 0000000000..c1e990a1b1
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/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: LocalizedStringKey
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift
new file mode 100644
index 0000000000..6d6614973f
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/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, 8)
+ .padding(.vertical, 4)
+ .background(
+ RoundedRectangle(cornerRadius: 8.0)
+ .stroke(
+ UIColor.primaryColor.color,
+ lineWidth: 1
+ )
+ .background(
+ RoundedRectangle(cornerRadius: 8.0)
+ .fill(UIColor.secondaryColor.color)
+ )
+ )
+ }
+}
+
+#Preview {
+ ZStack {
+ ChipView(item: ChipModel(name: LocalizedStringKey("Example")))
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(UIColor.secondaryColor.color)
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift
new file mode 100644
index 0000000000..65e3b0ccef
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift
@@ -0,0 +1,32 @@
+//
+// 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 }
+ var isExpanded: Bool { get set }
+}
+
+class MockFeatureIndicatorsViewModel: ChipViewModelProtocol {
+ @Published var chips: [ChipModel] = [
+ ChipModel(name: LocalizedStringKey("DAITA")),
+ ChipModel(name: LocalizedStringKey("Obfuscation")),
+ ChipModel(name: LocalizedStringKey("Quantum resistance")),
+ ChipModel(name: LocalizedStringKey("Multihop")),
+ ChipModel(name: LocalizedStringKey("DNS content blockers")),
+ ChipModel(name: LocalizedStringKey("Custom DNS")),
+ ChipModel(name: LocalizedStringKey("Server IP override")),
+ ]
+
+ @Published var isExpanded: Bool
+
+ init(isExpanded: Bool = false) {
+ self.isExpanded = isExpanded
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
index 03980fb361..3a1bf7d9af 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ConnectionView.swift
@@ -6,12 +6,14 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
+import MullvadSettings
import SwiftUI
typealias ButtonAction = (ConnectionViewViewModel.TunnelControlAction) -> Void
struct ConnectionView: View {
@StateObject var viewModel: ConnectionViewViewModel
+ @StateObject var indicatorsViewModel: FeatureIndicatorsViewModel
var action: ButtonAction?
var onContentUpdate: (() -> Void)?
@@ -27,6 +29,11 @@ struct ConnectionView: View {
VStack(alignment: .leading, spacing: 16) {
ConnectionPanel(viewModel: viewModel)
+
+ if !indicatorsViewModel.chips.isEmpty {
+ FeatureIndicatorsView(viewModel: indicatorsViewModel)
+ }
+
ButtonPanel(viewModel: viewModel, action: action)
}
.padding(16)
@@ -34,17 +41,24 @@ struct ConnectionView: View {
.cornerRadius(12)
.padding(16)
}
- .onReceive(viewModel.$tunnelState, perform: { _ in
+ .padding(.bottom, 8) // Adding some spacing so to not overlap with the map legal link.
+ .onReceive(
+ indicatorsViewModel.$isExpanded
+ .combineLatest(
+ viewModel.$tunnelState,
+ viewModel.$showsActivityIndicator
+ )
+ ) { _ in
onContentUpdate?()
- })
- .onReceive(viewModel.$showsActivityIndicator, perform: { _ in
- onContentUpdate?()
- })
+ }
}
}
#Preview {
- ConnectionView(viewModel: ConnectionViewViewModel(tunnelState: .disconnected)) { action in
+ ConnectionView(
+ viewModel: ConnectionViewViewModel(tunnelState: .disconnected),
+ indicatorsViewModel: FeatureIndicatorsViewModel(tunnelSettings: LatestTunnelSettings(), ipOverrides: [])
+ ) { action in
print(action)
}
.background(UIColor.secondaryColor.color)
diff --git a/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FI_TunnelViewController.swift
index b70c3a9ffa..9aed890041 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(tunnelState: tunnelState)
+ indicatorsViewViewModel = FeatureIndicatorsViewModel(
+ tunnelSettings: interactor.tunnelSettings,
+ ipOverrides: interactor.ipOverrides
+ )
+
+ connectionView = ConnectionView(
+ viewModel: self.connectionViewViewModel,
+ indicatorsViewModel: self.indicatorsViewViewModel
+ )
super.init(nibName: nil, bundle: nil)
@@ -73,10 +87,17 @@ class FI_TunnelViewController: UIViewController, RootContainment {
interactor.didUpdateTunnelStatus = { [weak self] tunnelStatus in
self?.setTunnelState(tunnelStatus.state, animated: true)
- self?.viewModel.tunnelState = tunnelStatus.state
self?.view.setNeedsLayout()
}
+ 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 +123,6 @@ class FI_TunnelViewController: UIViewController, RootContainment {
addMapController()
addContentView()
-
- tunnelState = interactor.tunnelStatus.state
- viewModel.tunnelState = tunnelState
-
updateMap(animated: false)
}
@@ -125,6 +142,8 @@ class FI_TunnelViewController: UIViewController, RootContainment {
private func setTunnelState(_ tunnelState: TunnelState, animated: Bool) {
self.tunnelState = tunnelState
+ connectionViewViewModel.tunnelState = tunnelState
+
setNeedsHeaderBarStyleAppearanceUpdate()
guard isViewLoaded else { return }
@@ -137,17 +156,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 +176,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
}
}
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..eb1a29ea81
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift
@@ -0,0 +1,31 @@
+//
+// 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
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ Text(LocalizedStringKey("Active features"))
+ .font(.footnote.weight(.semibold))
+ .foregroundStyle(UIColor.primaryTextColor.color.opacity(0.6))
+
+ ChipContainerView(viewModel: viewModel)
+ .onTapGesture {
+ viewModel.isExpanded.toggle()
+ }
+ }
+ }
+}
+
+#Preview("FeatureIndicatorsView") {
+ FeatureIndicatorsView(viewModel: MockFeatureIndicatorsViewModel(isExpanded: 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..42376b4560
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift
@@ -0,0 +1,37 @@
+//
+// FeatureIndicatorsViewModel.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-12-05.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadSettings
+
+class FeatureIndicatorsViewModel: ChipViewModelProtocol {
+ @Published var tunnelSettings: LatestTunnelSettings
+ @Published var ipOverrides: [IPOverride]
+ @Published var isExpanded = false
+
+ init(tunnelSettings: LatestTunnelSettings, ipOverrides: [IPOverride], isExpanded: Bool = false) {
+ self.tunnelSettings = tunnelSettings
+ self.ipOverrides = ipOverrides
+ self.isExpanded = isExpanded
+ }
+
+ 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/TunnelViewControllerInteractor.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift
index 47b75fd7d5..ef902e637a 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,9 +15,13 @@ 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 didUpdateTunnelSettings: ((LatestTunnelSettings) -> Void)?
+ var didUpdateIpOverrides: (([IPOverride]) -> Void)?
var didGetOutGoingAddress: (@MainActor (OutgoingConnectionInfo) -> Void)?
var tunnelStatus: 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
@@ -56,12 +70,21 @@ final class TunnelViewControllerInteractor {
},
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/MainButtonStyle.swift b/ios/MullvadVPN/Views/MainButtonStyle.swift
index f638c87ac2..f32a27fa06 100644
--- a/ios/MullvadVPN/Views/MainButtonStyle.swift
+++ b/ios/MullvadVPN/Views/MainButtonStyle.swift
@@ -22,16 +22,16 @@ struct MainButtonStyle: ButtonStyle {
.padding(.horizontal, 8)
.frame(height: 44)
.foregroundColor(
- configuration.isPressed
- ? UIColor.secondaryTextColor.color
- : disabled
- ? UIColor.primaryTextColor.withAlphaComponent(0.2).color
- : UIColor.primaryTextColor.color
+ disabled
+ ? UIColor.primaryTextColor.withAlphaComponent(0.2).color
+ : UIColor.primaryTextColor.color
)
.background(
disabled
- ? style.color.darkened(by: 0.6)
- : style.color
+ ? style.disabledColor
+ : configuration.isPressed
+ ? style.pressedColor
+ : style.color
)
.font(.body.weight(.semibold))
}
@@ -53,5 +53,13 @@ extension MainButtonStyle {
Color(UIColor.successColor)
}
}
+
+ var pressedColor: Color {
+ color.darkened(by: 0.4)!
+ }
+
+ var disabledColor: Color {
+ color.darkened(by: 0.6)!
+ }
}
}
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] = []) {