summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-01-30 16:29:55 +0100
committerBug Magnet <marco.nikic@mullvad.net>2024-01-30 16:29:55 +0100
commit21d1883687ed32f5abe60de062eb83ae31f97163 (patch)
treebc4a4b4a94e86e4c4a9cc4453881fe0f19c41ab2
parent2141af9ad1f8f71cc4abaad1c0942b2a6d1415d1 (diff)
parent851803b3f7f868faa11600663cf8c16c5e0b34b0 (diff)
downloadmullvadvpn-android/fix-devmole-google-play.tar.xz
mullvadvpn-android/fix-devmole-google-play.zip
Merge branch 'currently-in-use-api-access-method-should-be-visible-in-ui-ios-470'android/fix-devmole-google-play
-rw-r--r--ios/MullvadREST/Transport/AccessMethodIterator.swift17
-rw-r--r--ios/MullvadREST/Transport/LastReachableApiAccessCache.swift33
-rw-r--r--ios/MullvadREST/Transport/TransportStrategy.swift8
-rw-r--r--ios/MullvadRESTTests/AccessMethodRepositoryStub.swift14
-rw-r--r--ios/MullvadRESTTests/TransportStrategyTests.swift21
-rw-r--r--ios/MullvadSettings/AccessMethodRepository.swift86
-rw-r--r--ios/MullvadSettings/AccessMethodRepositoryProtocol.swift13
-rw-r--r--ios/MullvadSettings/PersistentAccessMethod.swift9
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj12
-rw-r--r--ios/MullvadVPN/AppDelegate.swift3
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentConfiguration.swift50
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentView.swift103
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift12
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractorProtocol.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift23
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractorProtocol.swift9
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodItem.swift3
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift43
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel+Persistent.swift2
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift1
22 files changed, 315 insertions, 153 deletions
diff --git a/ios/MullvadREST/Transport/AccessMethodIterator.swift b/ios/MullvadREST/Transport/AccessMethodIterator.swift
index 34ec668c64..531372efce 100644
--- a/ios/MullvadREST/Transport/AccessMethodIterator.swift
+++ b/ios/MullvadREST/Transport/AccessMethodIterator.swift
@@ -11,7 +11,6 @@ import Foundation
import MullvadSettings
class AccessMethodIterator {
- private var lastReachableApiAccessCache: LastReachableApiAccessCache
private let dataSource: AccessMethodRepositoryDataSource
private var index = 0
@@ -21,19 +20,15 @@ class AccessMethodIterator {
dataSource.fetchAll().filter { $0.isEnabled }
}
- private var lastReachableApiAccessId: UUID {
- lastReachableApiAccessCache.id
+ private var lastReachableApiAccessId: UUID? {
+ dataSource.fetchLastReachable().id
}
- init(_ userDefaults: UserDefaults, dataSource: AccessMethodRepositoryDataSource) {
+ init(dataSource: AccessMethodRepositoryDataSource) {
self.dataSource = dataSource
- self.lastReachableApiAccessCache = LastReachableApiAccessCache(
- defaultValue: dataSource.directAccess.id,
- container: userDefaults
- )
self.dataSource
- .publisher
+ .accessMethodsPublisher
.sink { [weak self] _ in
guard let self else { return }
self.refreshCacheIfNeeded()
@@ -48,14 +43,14 @@ class AccessMethodIterator {
} else {
/// When `firstIndex` is `nil`, that means the current configuration is not valid anymore
/// Invalidating cache by replacing the `current` to the next enabled access method
- lastReachableApiAccessCache.id = pick().id
+ dataSource.saveLastReachable(pick())
}
}
func rotate() {
let (partial, isOverflow) = index.addingReportingOverflow(1)
index = isOverflow ? 0 : partial
- lastReachableApiAccessCache.id = pick().id
+ dataSource.saveLastReachable(pick())
}
func pick() -> PersistentAccessMethod {
diff --git a/ios/MullvadREST/Transport/LastReachableApiAccessCache.swift b/ios/MullvadREST/Transport/LastReachableApiAccessCache.swift
deleted file mode 100644
index 100a591730..0000000000
--- a/ios/MullvadREST/Transport/LastReachableApiAccessCache.swift
+++ /dev/null
@@ -1,33 +0,0 @@
-//
-// LastReachableApiAccessStorage.swift
-// MullvadREST
-//
-// Created by Mojgan on 2024-01-08.
-// Copyright © 2024 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import MullvadSettings
-struct LastReachableApiAccessCache: Identifiable {
- /// `UserDefaults` key shared by both processes. Used to cache and synchronize last reachable api access method between them.
- private let key = "LastReachableConfigurationCacheKey"
- private var container: UserDefaults
- private let defaultValue: UUID
-
- init(defaultValue: UUID, container: UserDefaults) {
- self.container = container
- self.defaultValue = defaultValue
- }
-
- var id: UUID {
- get {
- guard let value = container.string(forKey: key) else {
- return defaultValue
- }
- return UUID(uuidString: value)!
- }
- set {
- container.set(newValue.uuidString, forKey: key)
- }
- }
-}
diff --git a/ios/MullvadREST/Transport/TransportStrategy.swift b/ios/MullvadREST/Transport/TransportStrategy.swift
index a41139a41d..72920ddcd5 100644
--- a/ios/MullvadREST/Transport/TransportStrategy.swift
+++ b/ios/MullvadREST/Transport/TransportStrategy.swift
@@ -11,7 +11,7 @@ import Logging
import MullvadSettings
import MullvadTypes
-public class TransportStrategy: Equatable {
+public struct TransportStrategy: Equatable {
/// The different transports suggested by the strategy
public enum Transport: Equatable {
/// Connecting a direct connection
@@ -45,15 +45,11 @@ public class TransportStrategy: Equatable {
private let accessMethodIterator: AccessMethodIterator
public init(
- _ userDefaults: UserDefaults,
datasource: AccessMethodRepositoryDataSource,
shadowsocksLoader: ShadowsocksLoaderProtocol
) {
self.shadowsocksLoader = shadowsocksLoader
- self.accessMethodIterator = AccessMethodIterator(
- userDefaults,
- dataSource: datasource
- )
+ self.accessMethodIterator = AccessMethodIterator(dataSource: datasource)
}
/// Rotating between enabled configurations by what order they were added in
diff --git a/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift b/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift
index bd8b341654..a2640df952 100644
--- a/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift
+++ b/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift
@@ -9,10 +9,10 @@
import Combine
import MullvadSettings
-typealias PersistentAccessMethod = MullvadSettings.PersistentAccessMethod
struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource {
- var directAccess: MullvadSettings.PersistentAccessMethod
- var publisher: AnyPublisher<[MullvadSettings.PersistentAccessMethod], Never> {
+ var directAccess: PersistentAccessMethod
+
+ var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> {
passthroughSubject.eraseToAnyPublisher()
}
@@ -23,7 +23,13 @@ struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource {
passthroughSubject.send(accessMethods)
}
- func fetchAll() -> [MullvadSettings.PersistentAccessMethod] {
+ func fetchAll() -> [PersistentAccessMethod] {
passthroughSubject.value
}
+
+ func saveLastReachable(_ method: PersistentAccessMethod) {}
+
+ func fetchLastReachable() -> PersistentAccessMethod {
+ directAccess
+ }
}
diff --git a/ios/MullvadRESTTests/TransportStrategyTests.swift b/ios/MullvadRESTTests/TransportStrategyTests.swift
index 721cd5b904..3174686ff6 100644
--- a/ios/MullvadRESTTests/TransportStrategyTests.swift
+++ b/ios/MullvadRESTTests/TransportStrategyTests.swift
@@ -12,22 +12,13 @@
import XCTest
class TransportStrategyTests: XCTestCase {
- var userDefaults: UserDefaults!
- static var suiteName: String!
-
private var directAccess: PersistentAccessMethod!
private var bridgeAccess: PersistentAccessMethod!
private var shadowsocksLoader: ShadowsocksLoaderStub!
- override class func setUp() {
- super.setUp()
- suiteName = UUID().uuidString
- }
-
override func setUpWithError() throws {
try super.setUpWithError()
- userDefaults = UserDefaults(suiteName: Self.suiteName)
shadowsocksLoader = ShadowsocksLoaderStub(configuration: ShadowsocksConfiguration(
address: .ipv4(.loopback),
@@ -51,16 +42,10 @@ class TransportStrategyTests: XCTestCase {
)
}
- override func tearDownWithError() throws {
- userDefaults.removePersistentDomain(forName: Self.suiteName)
- try super.tearDownWithError()
- }
-
func testDefaultStrategyIsDirectWhenAllMethodsAreDisabled() throws {
directAccess.isEnabled = false
bridgeAccess.isEnabled = false
let transportStrategy = TransportStrategy(
- userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
@@ -76,7 +61,6 @@ class TransportStrategyTests: XCTestCase {
func testReuseSameStrategyWhenEverythingElseIsDisabled() throws {
directAccess.isEnabled = false
let transportStrategy = TransportStrategy(
- userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
@@ -96,7 +80,6 @@ class TransportStrategyTests: XCTestCase {
func testLoopsFromTheStartAfterTryingAllEnabledStrategies() {
let transportStrategy = TransportStrategy(
- userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
@@ -130,7 +113,6 @@ class TransportStrategyTests: XCTestCase {
func testUsesNextWhenItIsNotReachable() {
bridgeAccess.isEnabled = false
let transportStrategy = TransportStrategy(
- userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
@@ -164,7 +146,6 @@ class TransportStrategyTests: XCTestCase {
func testGoToNextStrategyWhenItFailsToLoadBridgeConfiguration() {
shadowsocksLoader.error = IOError.fileNotFound
let transportStrategy = TransportStrategy(
- userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
@@ -180,7 +161,6 @@ class TransportStrategyTests: XCTestCase {
shadowsocksLoader.error = IOError.fileNotFound
directAccess.isEnabled = false
let transportStrategy = TransportStrategy(
- userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
@@ -207,7 +187,6 @@ class TransportStrategyTests: XCTestCase {
authentication: authentication
)
let transportStrategy = TransportStrategy(
- userDefaults,
datasource: AccessMethodRepositoryStub(accessMethods: [
directAccess,
bridgeAccess,
diff --git a/ios/MullvadSettings/AccessMethodRepository.swift b/ios/MullvadSettings/AccessMethodRepository.swift
index 0b03f03817..2df3ce7c89 100644
--- a/ios/MullvadSettings/AccessMethodRepository.swift
+++ b/ios/MullvadSettings/AccessMethodRepository.swift
@@ -27,10 +27,14 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol {
proxyConfiguration: .bridges
)
- let passthroughSubject: CurrentValueSubject<[PersistentAccessMethod], Never> = CurrentValueSubject([])
+ private let accessMethodsSubject: CurrentValueSubject<[PersistentAccessMethod], Never>
+ public var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> {
+ accessMethodsSubject.eraseToAnyPublisher()
+ }
- public var publisher: AnyPublisher<[PersistentAccessMethod], Never> {
- passthroughSubject.eraseToAnyPublisher()
+ private let lastReachableAccessMethodSubject: CurrentValueSubject<PersistentAccessMethod, Never>
+ public var lastReachableAccessMethodPublisher: AnyPublisher<PersistentAccessMethod, Never> {
+ lastReachableAccessMethodSubject.eraseToAnyPublisher()
}
public var directAccess: PersistentAccessMethod {
@@ -38,37 +42,57 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol {
}
public init() {
+ accessMethodsSubject = CurrentValueSubject([])
+ lastReachableAccessMethodSubject = CurrentValueSubject(direct)
+
add([direct, bridge])
+
+ accessMethodsSubject.send(fetchAll())
+ lastReachableAccessMethodSubject.send(fetchLastReachable())
}
public func save(_ method: PersistentAccessMethod) {
- var storedMethods = fetchAll()
+ var methodStore = readApiAccessMethodStore()
- if let index = storedMethods.firstIndex(where: { $0.id == method.id }) {
- storedMethods[index] = method
+ if let index = methodStore.accessMethods.firstIndex(where: { $0.id == method.id }) {
+ methodStore.accessMethods[index] = method
} else {
- storedMethods.append(method)
+ methodStore.accessMethods.append(method)
}
do {
- try writeApiAccessMethods(storedMethods)
+ try writeApiAccessMethodStore(methodStore)
+ accessMethodsSubject.send(methodStore.accessMethods)
} catch {
- logger.error("Could not update access methods: \(storedMethods) \nError: \(error)")
+ logger.error("Could not save access method: \(method) \nError: \(error)")
+ }
+ }
+
+ public func saveLastReachable(_ method: PersistentAccessMethod) {
+ var methodStore = readApiAccessMethodStore()
+ methodStore.lastReachableAccessMethod = method
+
+ do {
+ try writeApiAccessMethodStore(methodStore)
+ lastReachableAccessMethodSubject.send(method)
+ } catch {
+ logger.error("Could not save last reachable access method: \(method) \nError: \(error)")
}
}
public func delete(id: UUID) {
- var methods = fetchAll()
- guard let index = methods.firstIndex(where: { $0.id == id }) else { return }
+ var methodStore = readApiAccessMethodStore()
+ guard let index = methodStore.accessMethods.firstIndex(where: { $0.id == id }) else { return }
// Prevent removing methods that have static UUIDs and are always present.
- let method = methods[index]
+ let method = methodStore.accessMethods[index]
if !method.kind.isPermanent {
- methods.remove(at: index)
+ methodStore.accessMethods.remove(at: index)
}
do {
- try writeApiAccessMethods(methods)
+ try writeApiAccessMethodStore(methodStore)
+ accessMethodsSubject.send(methodStore.accessMethods)
} catch {
logger.error("Could not delete access method with id: \(id) \nError: \(error)")
}
@@ -79,7 +103,11 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol {
}
public func fetchAll() -> [PersistentAccessMethod] {
- (try? readApiAccessMethods()) ?? []
+ readApiAccessMethodStore().accessMethods
+ }
+
+ public func fetchLastReachable() -> PersistentAccessMethod {
+ readApiAccessMethodStore().lastReachableAccessMethod
}
public func reloadWithDefaultsAfterDataRemoval() {
@@ -87,35 +115,39 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol {
}
private func add(_ methods: [PersistentAccessMethod]) {
- var storedMethods = fetchAll()
+ var methodStore = readApiAccessMethodStore()
methods.forEach { method in
- if !storedMethods.contains(where: { $0.id == method.id }) {
- storedMethods.append(method)
+ if !methodStore.accessMethods.contains(where: { $0.id == method.id }) {
+ methodStore.accessMethods.append(method)
}
}
do {
- try writeApiAccessMethods(storedMethods)
+ try writeApiAccessMethodStore(methodStore)
+ accessMethodsSubject.send(methods)
} catch {
- logger.error("Could not update access methods: \(storedMethods) \nError: \(error)")
+ logger.error("Could not update access methods: \(methods) \nError: \(error)")
}
}
- private func readApiAccessMethods() throws -> [PersistentAccessMethod] {
+ private func readApiAccessMethodStore() -> PersistentAccessMethodStore {
let parser = makeParser()
- let data = try SettingsManager.store.read(key: .apiAccessMethods)
- return try parser.parseUnversionedPayload(as: [PersistentAccessMethod].self, from: data)
+ do {
+ let data = try SettingsManager.store.read(key: .apiAccessMethods)
+ return try parser.parseUnversionedPayload(as: PersistentAccessMethodStore.self, from: data)
+ } catch {
+ logger.error("Could not load access method store: \(error)")
+ return PersistentAccessMethodStore(lastReachableAccessMethod: direct, accessMethods: [])
+ }
}
- private func writeApiAccessMethods(_ accessMethods: [PersistentAccessMethod]) throws {
+ private func writeApiAccessMethodStore(_ store: PersistentAccessMethodStore) throws {
let parser = makeParser()
- let data = try parser.produceUnversionedPayload(accessMethods)
+ let data = try parser.produceUnversionedPayload(store)
try SettingsManager.store.write(data, for: .apiAccessMethods)
-
- passthroughSubject.send(accessMethods)
}
private func makeParser() -> SettingsParser {
diff --git a/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift b/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift
index 037ea24cda..02e0fa71f9 100644
--- a/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift
+++ b/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift
@@ -9,8 +9,8 @@
import Combine
public protocol AccessMethodRepositoryDataSource {
- /// Publisher that propagates a snapshot of persistent store upon modifications.
- var publisher: AnyPublisher<[PersistentAccessMethod], Never> { get }
+ /// Publisher that propagates a snapshot of all access methods upon modifications.
+ var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> { get }
/// - Returns: the default strategy.
var directAccess: PersistentAccessMethod { get }
@@ -18,9 +18,18 @@ public protocol AccessMethodRepositoryDataSource {
/// Fetch all access method from the persistent store.
/// - Returns: an array of all persistent access method.
func fetchAll() -> [PersistentAccessMethod]
+
+ /// Save last reachable access method to the persistent store.
+ func saveLastReachable(_ method: PersistentAccessMethod)
+
+ /// Fetch last reachable access method from the persistent store.
+ func fetchLastReachable() -> PersistentAccessMethod
}
public protocol AccessMethodRepositoryProtocol: AccessMethodRepositoryDataSource {
+ /// Publisher that propagates a snapshot of last reachable access method upon modifications.
+ var lastReachableAccessMethodPublisher: AnyPublisher<PersistentAccessMethod, Never> { get }
+
/// Add new access method.
/// - Parameter method: persistent access method model.
func save(_ method: PersistentAccessMethod)
diff --git a/ios/MullvadSettings/PersistentAccessMethod.swift b/ios/MullvadSettings/PersistentAccessMethod.swift
index 6728552375..9d67ce849f 100644
--- a/ios/MullvadSettings/PersistentAccessMethod.swift
+++ b/ios/MullvadSettings/PersistentAccessMethod.swift
@@ -10,6 +10,15 @@ import Foundation
import MullvadTypes
import Network
+/// Persistent access method container model.
+public struct PersistentAccessMethodStore: Codable {
+ /// The last successfully reached access method.
+ public var lastReachableAccessMethod: PersistentAccessMethod
+
+ /// Persistent access method models.
+ public var accessMethods: [PersistentAccessMethod]
+}
+
/// Persistent access method model.
public struct PersistentAccessMethod: Identifiable, Codable, Equatable {
/// The unique identifier used for referencing the access method entry in a persistent store.
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index f65b281bb3..b84f9dba52 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -499,6 +499,8 @@
7A5869C72B5A8E4C00640D27 /* MethodSettingsDataSourceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869C62B5A8E4C00640D27 /* MethodSettingsDataSourceConfiguration.swift */; };
7A6000F62B60092F001CF0D9 /* AccessMethodViewModelEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6000F52B60092F001CF0D9 /* AccessMethodViewModelEditing.swift */; };
7A6000F92B6273A4001CF0D9 /* AccessMethodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D7B2B03BDD100E7CDD7 /* AccessMethodViewModel.swift */; };
+ 7A6000FC2B628DF6001CF0D9 /* ListCellContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6000FB2B628DF6001CF0D9 /* ListCellContentConfiguration.swift */; };
+ 7A6000FE2B628E9F001CF0D9 /* ListCellContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6000FD2B628E9F001CF0D9 /* ListCellContentView.swift */; };
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; };
7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; };
7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; };
@@ -737,7 +739,6 @@
F0164EBA2B4456D30020268D /* AccessMethodRepositoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */; };
F0164EBC2B482E430020268D /* AppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBB2B482E430020268D /* AppStorage.swift */; };
F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */; };
- F0164EC12B4C03980020268D /* LastReachableApiAccessCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC02B4C03980020268D /* LastReachableApiAccessCache.swift */; };
F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */; };
F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */; };
F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */; };
@@ -1660,6 +1661,8 @@
7A5869C42B5A899C00640D27 /* MethodSettingsCellConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsCellConfiguration.swift; sourceTree = "<group>"; };
7A5869C62B5A8E4C00640D27 /* MethodSettingsDataSourceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsDataSourceConfiguration.swift; sourceTree = "<group>"; };
7A6000F52B60092F001CF0D9 /* AccessMethodViewModelEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodViewModelEditing.swift; sourceTree = "<group>"; };
+ 7A6000FB2B628DF6001CF0D9 /* ListCellContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCellContentConfiguration.swift; sourceTree = "<group>"; };
+ 7A6000FD2B628E9F001CF0D9 /* ListCellContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCellContentView.swift; sourceTree = "<group>"; };
7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = "<group>"; };
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = "<group>"; };
7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; };
@@ -1799,7 +1802,6 @@
F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodRepositoryStub.swift; sourceTree = "<group>"; };
F0164EBB2B482E430020268D /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = "<group>"; };
F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoader.swift; sourceTree = "<group>"; };
- F0164EC02B4C03980020268D /* LastReachableApiAccessCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastReachableApiAccessCache.swift; sourceTree = "<group>"; };
F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderStub.swift; sourceTree = "<group>"; };
F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodIterator.swift; sourceTree = "<group>"; };
F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewController.swift; sourceTree = "<group>"; };
@@ -2620,6 +2622,8 @@
58FF9FE92B07653800E4C97D /* ButtonCellContentView.swift */,
58CEB3092AFD584700E6E088 /* CustomCellDisclosureHandling.swift */,
58CEB30B2AFD586600E6E088 /* DynamicBackgroundConfiguration.swift */,
+ 7A6000FB2B628DF6001CF0D9 /* ListCellContentConfiguration.swift */,
+ 7A6000FD2B628E9F001CF0D9 /* ListCellContentView.swift */,
58CEB3012AFD365600E6E088 /* SwitchCellContentConfiguration.swift */,
58CEB3032AFD36CE00E6E088 /* SwitchCellContentView.swift */,
58CEB2F42AFD0BB500E6E088 /* TextCellContentConfiguration.swift */,
@@ -3422,7 +3426,6 @@
F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */,
A932D9EE2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift */,
F0DC77A32B2315800087F09D /* Direct */,
- F0164EC02B4C03980020268D /* LastReachableApiAccessCache.swift */,
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */,
58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */,
F0DC77A22B2314EF0087F09D /* Shadowsocks */,
@@ -4357,7 +4360,6 @@
06799AE728F98E4800ACD94E /* RESTURLSession.swift in Sources */,
A90763B52B2857D50045ADF0 /* Socks5Constants.swift in Sources */,
A90763BA2B2857D50045ADF0 /* Socks5Error.swift in Sources */,
- F0164EC12B4C03980020268D /* LastReachableApiAccessCache.swift in Sources */,
06799AF428F98E4800ACD94E /* RESTAuthorization.swift in Sources */,
06799AE228F98E4800ACD94E /* RESTRequestFactory.swift in Sources */,
A90763BD2B2857D50045ADF0 /* Socks5Connection.swift in Sources */,
@@ -4772,6 +4774,7 @@
7AC8A3AE2ABC6FBB00DC4939 /* SettingsHeaderView.swift in Sources */,
588D7EDC2AF3A55E005DF40A /* ListAccessMethodInteractorProtocol.swift in Sources */,
588D7ED62AF3903F005DF40A /* ListAccessMethodViewController.swift in Sources */,
+ 7A6000FC2B628DF6001CF0D9 /* ListCellContentConfiguration.swift in Sources */,
582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */,
7A9FA1442A2E3FE5000B728D /* CheckableSettingsCell.swift in Sources */,
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */,
@@ -4831,6 +4834,7 @@
7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */,
7A6F2FAB2AFD3097006D0856 /* CustomDNSCellFactory.swift in Sources */,
58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */,
+ 7A6000FE2B628E9F001CF0D9 /* ListCellContentView.swift in Sources */,
58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */,
587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */,
7A9CCCBE2A96302800DD6A34 /* AccountDeletionCoordinator.swift in Sources */,
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index e7847a7623..7a1ddb1358 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -91,8 +91,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
let urlSessionTransport = URLSessionTransport(urlSession: REST.makeURLSession())
let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL)
- // This init cannot fail as long as the security group identifier is valid
- let sharedUserDefaults = UserDefaults(suiteName: ApplicationConfiguration.securityGroupIdentifier)!
shadowsocksLoader = ShadowsocksLoader(
shadowsocksCache: shadowsocksCache,
relayCache: relayCache,
@@ -105,7 +103,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
)
let transportStrategy = TransportStrategy(
- sharedUserDefaults,
datasource: accessMethodRepository,
shadowsocksLoader: shadowsocksLoader
)
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentConfiguration.swift
new file mode 100644
index 0000000000..891927c510
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentConfiguration.swift
@@ -0,0 +1,50 @@
+//
+// ListCellContentConfiguration.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-01-25.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+/// Content configuration presenting a label and switch control.
+struct ListCellContentConfiguration: UIContentConfiguration, Equatable {
+ struct TextProperties: Equatable {
+ var font = UIFont.systemFont(ofSize: 17)
+ var color = UIColor.Cell.titleTextColor
+ }
+
+ struct SecondaryTextProperties: Equatable {
+ var font = UIFont.systemFont(ofSize: 17)
+ var color = UIColor.Cell.detailTextColor.withAlphaComponent(0.8)
+ }
+
+ struct TertiaryTextProperties: Equatable {
+ var font = UIFont.systemFont(ofSize: 15)
+ var color = UIColor.Cell.titleTextColor.withAlphaComponent(0.6)
+ }
+
+ /// Primary text label.
+ var text: String?
+ let textProperties = TextProperties()
+
+ /// Secondary (trailing) text label.
+ var secondaryText: String?
+ let secondaryTextProperties = SecondaryTextProperties()
+
+ /// Tertiary (below primary) text label.
+ var tertiaryText: String?
+ let tertiaryTextProperties = TertiaryTextProperties()
+
+ /// Content view layout margins.
+ var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.apiAccessInsetLayoutMargins
+
+ func makeContentView() -> UIView & UIContentView {
+ return ListCellContentView(configuration: self)
+ }
+
+ func updated(for state: UIConfigurationState) -> Self {
+ return self
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentView.swift
new file mode 100644
index 0000000000..2702d38a3e
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentView.swift
@@ -0,0 +1,103 @@
+//
+// ListCellContentView.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-01-25.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+/// Content view presenting a primary, secondary (trailing) and tertiary (below primary) label.
+class ListCellContentView: UIView, UIContentView, UITextFieldDelegate {
+ private var textLabel = UILabel()
+ private var secondaryTextLabel = UILabel()
+ private var tertiaryTextLabel = UILabel()
+
+ var configuration: UIContentConfiguration {
+ get {
+ actualConfiguration
+ }
+ set {
+ guard let newConfiguration = newValue as? ListCellContentConfiguration,
+ actualConfiguration != newConfiguration else { return }
+
+ let previousConfiguration = actualConfiguration
+ actualConfiguration = newConfiguration
+
+ configureSubviews(previousConfiguration: previousConfiguration)
+ }
+ }
+
+ private var actualConfiguration: ListCellContentConfiguration
+
+ func supports(_ configuration: UIContentConfiguration) -> Bool {
+ configuration is ListCellContentConfiguration
+ }
+
+ init(configuration: ListCellContentConfiguration) {
+ actualConfiguration = configuration
+
+ super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 0))
+
+ configureSubviews()
+ addSubviews()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func configureSubviews(previousConfiguration: ListCellContentConfiguration? = nil) {
+ configureTextLabel()
+ configureSecondaryTextLabel()
+ configureTertiaryTextLabel()
+ configureLayoutMargins()
+ }
+
+ private func configureTextLabel() {
+ let textProperties = actualConfiguration.textProperties
+
+ textLabel.font = textProperties.font
+ textLabel.textColor = textProperties.color
+
+ textLabel.text = actualConfiguration.text
+ }
+
+ private func configureSecondaryTextLabel() {
+ let textProperties = actualConfiguration.secondaryTextProperties
+
+ secondaryTextLabel.font = textProperties.font
+ secondaryTextLabel.textColor = textProperties.color
+
+ secondaryTextLabel.text = actualConfiguration.secondaryText
+ }
+
+ private func configureTertiaryTextLabel() {
+ let textProperties = actualConfiguration.tertiaryTextProperties
+
+ tertiaryTextLabel.font = textProperties.font
+ tertiaryTextLabel.textColor = textProperties.color
+
+ tertiaryTextLabel.text = actualConfiguration.tertiaryText
+ }
+
+ private func configureLayoutMargins() {
+ directionalLayoutMargins = actualConfiguration.directionalLayoutMargins
+ }
+
+ private func addSubviews() {
+ let leadingTextContainer = UIStackView(arrangedSubviews: [textLabel, tertiaryTextLabel])
+ leadingTextContainer.axis = .vertical
+
+ addConstrainedSubviews([leadingTextContainer, secondaryTextLabel]) {
+ leadingTextContainer.pinEdgesToSuperviewMargins(.all().excluding(.trailing))
+ leadingTextContainer.centerYAnchor.constraint(equalTo: centerYAnchor)
+ secondaryTextLabel.pinEdgesToSuperviewMargins(.all().excluding(.leading))
+ secondaryTextLabel.leadingAnchor.constraint(
+ greaterThanOrEqualToSystemSpacingAfter: leadingTextContainer.trailingAnchor,
+ multiplier: 1
+ )
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift
index 59650aa15d..e62c56ed12 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift
@@ -8,6 +8,6 @@
import MullvadSettings
-protocol AccessMethodEditing {
+protocol AccessMethodEditing: AnyObject {
func accessMethodDidSave(_ accessMethod: PersistentAccessMethod)
}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift
index b33e3c227c..420fd71eab 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift
@@ -15,14 +15,6 @@ struct EditAccessMethodInteractor: EditAccessMethodInteractorProtocol {
let repository: AccessMethodRepositoryProtocol
let proxyConfigurationTester: ProxyConfigurationTesterProtocol
- var directAccess: PersistentAccessMethod {
- repository.directAccess
- }
-
- var publisher: AnyPublisher<[PersistentAccessMethod], Never> {
- repository.publisher.eraseToAnyPublisher()
- }
-
func saveAccessMethod() {
guard let persistentMethod = try? subject.value.intoPersistentAccessMethod() else { return }
@@ -33,10 +25,6 @@ struct EditAccessMethodInteractor: EditAccessMethodInteractorProtocol {
repository.delete(id: subject.value.id)
}
- func fetchAll() -> [MullvadSettings.PersistentAccessMethod] {
- return repository.fetchAll()
- }
-
func startProxyConfigurationTest(_ completion: ((Bool) -> Void)?) {
guard let config = try? subject.value.intoPersistentProxyConfiguration() else { return }
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractorProtocol.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractorProtocol.swift
index 870224bd8e..a5a68db881 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractorProtocol.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractorProtocol.swift
@@ -9,7 +9,7 @@
import MullvadSettings
/// The type implementing the interface for persisting changes to the underlying access method view model in the editing context.
-protocol EditAccessMethodInteractorProtocol: ProxyConfigurationInteractorProtocol, AccessMethodRepositoryDataSource {
+protocol EditAccessMethodInteractorProtocol: ProxyConfigurationInteractorProtocol {
/// Save changes to persistent store.
///
/// - Calling this method when the underlying view model fails validation does nothing.
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift
index aee945cc76..9c0f844786 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift
@@ -8,7 +8,7 @@
import Foundation
-protocol EditAccessMethodViewControllerDelegate: AnyObject {
+protocol EditAccessMethodViewControllerDelegate: AnyObject, AccessMethodEditing {
/// The view controller requests the delegate to present the proxy configuration view controller.
/// - Parameter controller: the calling controller.
func controllerShouldShowMethodSettings(_ controller: EditAccessMethodViewController)
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift
index 4cdc30afbb..d06a804d3b 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift
@@ -11,24 +11,30 @@ import MullvadSettings
/// A concrete implementation of an API access list interactor.
struct ListAccessMethodInteractor: ListAccessMethodInteractorProtocol {
- let reporepository: AccessMethodRepositoryProtocol
+ let repository: AccessMethodRepositoryProtocol
init(repository: AccessMethodRepositoryProtocol) {
- self.reporepository = repository
+ self.repository = repository
}
- var publisher: any Publisher<[ListAccessMethodItem], Never> {
- reporepository.publisher.map { newElements in
- newElements.map { $0.toListItem() }
+ var itemsPublisher: any Publisher<[ListAccessMethodItem], Never> {
+ repository.accessMethodsPublisher.map { methods in
+ methods.map { $0.toListItem() }
+ }
+ }
+
+ var itemInUsePublisher: any Publisher<ListAccessMethodItem?, Never> {
+ repository.lastReachableAccessMethodPublisher.map { method in
+ method.toListItem()
}
}
func item(by id: UUID) -> ListAccessMethodItem? {
- reporepository.fetch(by: id)?.toListItem()
+ repository.fetch(by: id)?.toListItem()
}
func fetch() -> [ListAccessMethodItem] {
- reporepository.fetchAll().map { $0.toListItem() }
+ repository.fetchAll().map { $0.toListItem() }
}
}
@@ -47,7 +53,8 @@ extension PersistentAccessMethod {
tableName: "APIAccess",
value: "Disabled",
comment: ""
- )
+ ),
+ isEnabled: isEnabled
)
}
}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractorProtocol.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractorProtocol.swift
index aca4e40968..91e766341b 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractorProtocol.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractorProtocol.swift
@@ -11,12 +11,15 @@ import MullvadSettings
/// Types describing API access list interactor.
protocol ListAccessMethodInteractorProtocol {
+ /// Publisher that produces a list of method items upon persistent store modifications.
+ var itemsPublisher: any Publisher<[ListAccessMethodItem], Never> { get }
+
+ /// Publisher that produces the last reachable method item upon persistent store modifications.
+ var itemInUsePublisher: any Publisher<ListAccessMethodItem?, Never> { get }
+
/// Returns an item by id.
func item(by id: UUID) -> ListAccessMethodItem?
/// Fetch all items.
func fetch() -> [ListAccessMethodItem]
-
- /// Publisher that produces a list of method items upon persisrtent store modifications.
- var publisher: any Publisher<[ListAccessMethodItem], Never> { get }
}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodItem.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodItem.swift
index 44367754ce..bcd536062e 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodItem.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodItem.swift
@@ -17,4 +17,7 @@ struct ListAccessMethodItem: Hashable, Identifiable, Equatable {
/// The detailed information displayed alongside.
let detail: String?
+
+ /// Whether method is enabled or not.
+ let isEnabled: Bool
}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift
index 9e6e31f497..f4340e7877 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift
@@ -7,6 +7,8 @@
//
import Combine
+import MullvadREST
+import MullvadSettings
import UIKit
enum ListAccessMethodSectionIdentifier: Hashable {
@@ -26,6 +28,7 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate {
private let headerView = ListAccessMethodHeaderView()
private let interactor: ListAccessMethodInteractorProtocol
+ private var lastReachableMethodItem: ListAccessMethodItem?
private var cancellables = Set<AnyCancellable>()
private var dataSource: ListAccessMethodDataSource?
@@ -73,8 +76,14 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate {
addChild(contentController)
contentController.didMove(toParent: self)
- interactor.publisher.sink { _ in
- self.updateDataSource(animated: true)
+ interactor.itemsPublisher.sink { [weak self] _ in
+ self?.updateDataSource(animated: true)
+ }
+ .store(in: &cancellables)
+
+ interactor.itemInUsePublisher.sink { [weak self] item in
+ self?.lastReachableMethodItem = item
+ self?.updateDataSource(animated: true)
}
.store(in: &cancellables)
@@ -82,6 +91,16 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate {
configureDataSource()
}
+ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+ guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return 0 }
+
+ if itemIdentifier.id == lastReachableMethodItem?.id {
+ return UITableView.automaticDimension
+ } else {
+ return UIMetrics.SettingsCell.apiAccessCellHeight
+ }
+ }
+
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let container = UIView()
@@ -136,25 +155,18 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate {
}
private func updateDataSource(animated: Bool = true) {
- let oldFetchedItems = fetchedItems
- let newFetchedItems = interactor.fetch()
- fetchedItems = newFetchedItems
+ fetchedItems = interactor.fetch()
var snapshot = NSDiffableDataSourceSnapshot<ListAccessMethodSectionIdentifier, ListAccessMethodItemIdentifier>()
snapshot.appendSections([.primary])
- let itemIdentifiers = newFetchedItems.map { item in
+ let itemIdentifiers = fetchedItems.map { item in
ListAccessMethodItemIdentifier(id: item.id)
}
snapshot.appendItems(itemIdentifiers, toSection: .primary)
- for newFetchedItem in newFetchedItems {
- for oldFetchedItem in oldFetchedItems {
- if newFetchedItem.id == oldFetchedItem.id,
- newFetchedItem.name != oldFetchedItem.name || newFetchedItem.detail != oldFetchedItem.detail {
- snapshot.reloadItems([ListAccessMethodItemIdentifier(id: newFetchedItem.id)])
- }
- }
+ for item in fetchedItems {
+ snapshot.reloadItems([ListAccessMethodItemIdentifier(id: item.id)])
}
dataSource?.apply(snapshot, animatingDifferences: animated)
@@ -167,9 +179,12 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate {
let cell = tableView.dequeueReusableView(withIdentifier: CellReuseIdentifier.default, for: indexPath)
let item = fetchedItems[indexPath.row]
- var contentConfiguration = UIListContentConfiguration.mullvadValueCell(tableStyle: .plain)
+ var contentConfiguration = ListCellContentConfiguration()
contentConfiguration.text = item.name
contentConfiguration.secondaryText = item.detail
+ contentConfiguration.tertiaryText = lastReachableMethodItem?.id == item.id
+ ? NSLocalizedString("LIST_ACCESS_METHODS_IN_USE_ITEM", tableName: "APIAccess", value: "In use", comment: "")
+ : ""
cell.contentConfiguration = contentConfiguration
if let cell = cell as? DynamicBackgroundConfiguration {
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel+Persistent.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel+Persistent.swift
index 7e25a36f3d..3a1d938e05 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel+Persistent.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel+Persistent.swift
@@ -28,7 +28,7 @@ extension AccessMethodViewModel {
do {
configuration = try intoPersistentProxyConfiguration()
- } catch var error as AccessMethodValidationError {
+ } catch let error as AccessMethodValidationError {
var fieldErrors = error.fieldErrors
do {
diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
index fc0dcce43f..3cbf52c675 100644
--- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
@@ -42,7 +42,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// This init cannot fail as long as the security group identifier is valid
let transportStrategy = TransportStrategy(
- UserDefaults(suiteName: ApplicationConfiguration.securityGroupIdentifier)!,
datasource: AccessMethodRepository(),
shadowsocksLoader: ShadowsocksLoader(
shadowsocksCache: shadowsocksCache,