From 49f83d2a4dd3ff8f1081f7843ae290d806615664 Mon Sep 17 00:00:00 2001 From: mojganii Date: Wed, 11 Dec 2024 15:13:09 +0100 Subject: [PATCH] Add FeaturesIndicatoresView --- .../IPOverrideRepository.swift | 10 +- .../WireGuardObfuscationSettings.swift | 4 + ios/MullvadVPN.xcodeproj/project.pbxproj | 53 ++++++++--- .../Coordinators/ApplicationCoordinator.swift | 3 +- .../Coordinators/TunnelCoordinator.swift | 7 +- .../FeatureIndicators/ChipFeatures.swift | 88 +++++++++++++++++ .../ChipView/ChipContainerView.swift | 95 +++++++++++++++++++ .../ChipView/ChipModel.swift | 15 +++ .../FeatureIndicators/ChipView/ChipView.swift | 40 ++++++++ .../ChipView/ChipViewModelProtocol.swift | 32 +++++++ .../FeatureIndicators/ConnectionView.swift | 26 +++-- .../FI_TunnelViewController.swift | 45 ++++++--- .../FeatureIndicatorsView.swift | 31 ++++++ .../FeatureIndicatorsViewModel.swift | 37 ++++++++ .../TunnelViewControllerInteractor.swift | 27 +++++- ios/MullvadVPN/Views/MainButtonStyle.swift | 22 +++-- .../IPOverrideRepositoryStub.swift | 6 ++ 17 files changed, 496 insertions(+), 45 deletions(-) create mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipFeatures.swift create mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipContainerView.swift create mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipModel.swift create mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift create mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipViewModelProtocol.swift create mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsView.swift create mode 100644 ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/FeatureIndicatorsViewModel.swift diff --git a/ios/MullvadSettings/IPOverrideRepository.swift b/ios/MullvadSettings/IPOverrideRepository.swift index 867a1c077fd5..441ff6c35e1e 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 f067114cc634..c52a637626fd 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 fe09f04dafcd..8b27c68b6ee1 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -659,10 +659,10 @@ 7AF9BE902A39F26000DBFEDB /* Collection+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */; }; 7AF9BE952A40461100DBFEDB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE942A40461100DBFEDB /* RelayFilterView.swift */; }; 7AF9BE972A41C71F00DBFEDB /* ChipViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE962A41C71F00DBFEDB /* ChipViewCell.swift */; }; - 7AFBE38B2D09AAFF002335FC /* SinglehopPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */; }; - 7AFBE38D2D09AB2E002335FC /* MultihopPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */; }; 7AFBE3872D084C9D002335FC /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */; }; 7AFBE3892D089163002335FC /* FI_TunnelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE3882D08915D002335FC /* FI_TunnelViewController.swift */; }; + 7AFBE38B2D09AAFF002335FC /* SinglehopPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */; }; + 7AFBE38D2D09AB2E002335FC /* MultihopPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */; }; 850201DB2B503D7700EF8C96 /* RelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DA2B503D7700EF8C96 /* RelayTests.swift */; }; 850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */; }; 850201DF2B5040A500EF8C96 /* TunnelControlPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */; }; @@ -997,7 +997,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 */; }; @@ -2020,10 +2027,10 @@ 7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Sorting.swift"; sourceTree = ""; }; 7AF9BE942A40461100DBFEDB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = ""; }; 7AF9BE962A41C71F00DBFEDB /* ChipViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewCell.swift; sourceTree = ""; }; - 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SinglehopPicker.swift; sourceTree = ""; }; - 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopPicker.swift; sourceTree = ""; }; 7AFBE3862D084C96002335FC /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 7AFBE3882D08915D002335FC /* FI_TunnelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FI_TunnelViewController.swift; sourceTree = ""; }; + 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SinglehopPicker.swift; sourceTree = ""; }; + 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopPicker.swift; sourceTree = ""; }; 85006A8E2B73EF67004AD8FB /* MullvadVPNUITestsSmoke.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MullvadVPNUITestsSmoke.xctestplan; sourceTree = ""; }; 850201DA2B503D7700EF8C96 /* RelayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayTests.swift; sourceTree = ""; }; 850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationPage.swift; sourceTree = ""; }; @@ -2236,7 +2243,14 @@ F0ADC3712CD3AD1600A1AD97 /* ChipCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipCollectionView.swift; sourceTree = ""; }; F0ADC3732CD3C47400A1AD97 /* ChipFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFlowLayout.swift; sourceTree = ""; }; F0ADF1CC2CFDFF3100299F09 /* StringConversionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringConversionError.swift; sourceTree = ""; }; + F0ADF1D02D01B55C00299F09 /* ChipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipModel.swift; sourceTree = ""; }; + F0ADF1D22D01B6B400299F09 /* FeatureIndicatorsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIndicatorsViewModel.swift; sourceTree = ""; }; + F0ADF1D42D01DCFD00299F09 /* ChipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipView.swift; sourceTree = ""; }; F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Async.swift"; sourceTree = ""; }; + F0B495752D02025200CFEC2A /* ChipContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipContainerView.swift; sourceTree = ""; }; + F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewModelProtocol.swift; sourceTree = ""; }; + F0B495792D02F41F00CFEC2A /* ChipFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipFeatures.swift; sourceTree = ""; }; + F0B4957B2D03154200CFEC2A /* FeatureIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIndicatorsView.swift; sourceTree = ""; }; F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithLocation.swift; sourceTree = ""; }; F0B894F02BF751E300817A42 /* RelayWithDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayWithDistance.swift; sourceTree = ""; }; F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelaySelector+Wireguard.swift"; sourceTree = ""; }; @@ -3624,7 +3638,6 @@ A9D9A4C12C36D53C004088DD /* MullvadRustRuntimeTests */, 58CE5E61224146200008646E /* Products */, 584F991F2902CBDD001F858D /* Frameworks */, - 7A0EAE982D01B29E00D3EB8B /* Recovered References */, ); sourceTree = ""; }; @@ -3925,14 +3938,6 @@ path = Edit; sourceTree = ""; }; - 7A0EAE982D01B29E00D3EB8B /* Recovered References */ = { - isa = PBXGroup; - children = ( - 7AA1309C2D0072F900640DF9 /* View+Size.swift */, - ); - name = "Recovered References"; - sourceTree = ""; - }; 7A2960F72A964A3500389B82 /* Alert */ = { isa = PBXGroup; children = ( @@ -4067,9 +4072,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; @@ -4392,6 +4401,17 @@ path = MullvadTypes; sourceTree = ""; }; + F0ADF1CF2D01B50B00299F09 /* ChipView */ = { + isa = PBXGroup; + children = ( + F0B495752D02025200CFEC2A /* ChipContainerView.swift */, + F0ADF1D02D01B55C00299F09 /* ChipModel.swift */, + F0ADF1D42D01DCFD00299F09 /* ChipView.swift */, + F0B495772D02038B00CFEC2A /* ChipViewModelProtocol.swift */, + ); + path = ChipView; + sourceTree = ""; + }; F0DC779F2B2222D20087F09D /* Relay */ = { isa = PBXGroup; children = ( @@ -5917,6 +5937,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 */, @@ -5991,6 +6012,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 */, @@ -6100,6 +6122,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 */, @@ -6134,8 +6157,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 */, @@ -6179,6 +6204,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 */, @@ -6198,6 +6224,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 a4ff7e3cd0f5..bbab9203012d 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 1e3e48e64c73..0c55f7e1af46 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 @@ -32,13 +33,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 ) #if DEBUG 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 000000000000..c005b3f080ed --- /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 000000000000..f06124ed4811 --- /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: 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 000000000000..c1e990a1b1d9 --- /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 000000000000..f8b2e563bd8d --- /dev/null +++ b/ios/MullvadVPN/View controllers/Tunnel/FeatureIndicators/ChipView/ChipView.swift @@ -0,0 +1,40 @@ +// +// ChipView.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 000000000000..65e3b0ccef38 --- /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 03980fb361fd..3a1bf7d9afe3 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 b70c3a9ffa37..9aed89004110 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? @@ -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 000000000000..eb1a29ea8195 --- /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: 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 000000000000..42376b45608b --- /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 47b75fd7d5ef..ef902e637a44 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? + private var ipOverrideRepository: IPOverrideRepositoryProtocol + private var cancellables: Set = [] 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 f638c87ac2b5..f32a27fa068a 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 633bc44bdb5f..c27c5cff5837 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] = []) {