Refactor AppContext creation and profile processing (#810)
Streamline initialization of AppContext objects without singletons, especially because some are interconnected. Rethink ProfileProcessor to be the only gateway of profile processing for: - Include - Save - Connect Provide closures with access to the IAPManager for eligibility checks. Finally, take a ProfileProcessor parameter in: - ProfileManager (for isIncluded and willSave) - ExtendedTunnel (for willConnect) so that it's used implicitly without having to put it into the SwiftUI environment. Other than that: - Move AppError to CommonLibrary - Skip decoding of attributes from Core Data because they are already part of the profile
This commit is contained in:
parent
0c66050726
commit
f3d13d0cdf
|
@ -85,7 +85,6 @@ extension PassepartoutApp {
|
||||||
MenuBarExtra {
|
MenuBarExtra {
|
||||||
AppMenu(
|
AppMenu(
|
||||||
profileManager: context.profileManager,
|
profileManager: context.profileManager,
|
||||||
profileProcessor: context.profileProcessor,
|
|
||||||
tunnel: context.tunnel
|
tunnel: context.tunnel
|
||||||
)
|
)
|
||||||
.withEnvironment(from: context, theme: theme)
|
.withEnvironment(from: context, theme: theme)
|
||||||
|
|
|
@ -68,13 +68,7 @@ private extension AppData {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let profile = try registry.decodedProfile(from: encoded, with: coder)
|
let profile = try registry.decodedProfile(from: encoded, with: coder)
|
||||||
var builder = profile.builder()
|
return profile
|
||||||
builder.attributes = ProfileAttributes(
|
|
||||||
isAvailableForTV: cdEntity.isAvailableForTV?.boolValue ?? false,
|
|
||||||
lastUpdate: cdEntity.lastUpdate,
|
|
||||||
fingerprint: cdEntity.fingerprint
|
|
||||||
)
|
|
||||||
return try builder.tryBuild()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func toMapper(
|
static func toMapper(
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import CommonUtils
|
import CommonUtils
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -30,9 +30,6 @@ import SwiftUI
|
||||||
|
|
||||||
struct ProviderEntitySelector: View {
|
struct ProviderEntitySelector: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var profileProcessor: ProfileProcessor
|
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var profileManager: ProfileManager
|
var profileManager: ProfileManager
|
||||||
|
|
||||||
|
|
|
@ -30,9 +30,6 @@ import SwiftUI
|
||||||
|
|
||||||
struct TunnelRestartButton<Label>: View where Label: View {
|
struct TunnelRestartButton<Label>: View where Label: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var profileProcessor: ProfileProcessor
|
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var tunnel: ExtendedTunnel
|
var tunnel: ExtendedTunnel
|
||||||
|
|
||||||
|
@ -52,7 +49,7 @@ struct TunnelRestartButton<Label>: View where Label: View {
|
||||||
}
|
}
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await tunnel.connect(with: profile, processor: profileProcessor)
|
try await tunnel.connect(with: profile)
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
//
|
//
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -35,18 +35,14 @@ public struct AppMenu: View {
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
private var profileManager: ProfileManager
|
private var profileManager: ProfileManager
|
||||||
|
|
||||||
@ObservedObject
|
|
||||||
private var profileProcessor: ProfileProcessor
|
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
private var tunnel: ExtendedTunnel
|
private var tunnel: ExtendedTunnel
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var model = Model()
|
private var model = Model()
|
||||||
|
|
||||||
public init(profileManager: ProfileManager, profileProcessor: ProfileProcessor, tunnel: ExtendedTunnel) {
|
public init(profileManager: ProfileManager, tunnel: ExtendedTunnel) {
|
||||||
self.profileManager = profileManager
|
self.profileManager = profileManager
|
||||||
self.profileProcessor = profileProcessor
|
|
||||||
self.tunnel = tunnel
|
self.tunnel = tunnel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +92,7 @@ private extension AppMenu {
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
if isOn {
|
if isOn {
|
||||||
try await tunnel.connect(with: profile, processor: profileProcessor)
|
try await tunnel.connect(with: profile)
|
||||||
} else {
|
} else {
|
||||||
try await tunnel.disconnect()
|
try await tunnel.disconnect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,8 @@ public final class ExtendedTunnel: ObservableObject {
|
||||||
|
|
||||||
private let environment: TunnelEnvironment
|
private let environment: TunnelEnvironment
|
||||||
|
|
||||||
|
private let processor: ProfileProcessor?
|
||||||
|
|
||||||
private let interval: TimeInterval
|
private let interval: TimeInterval
|
||||||
|
|
||||||
public func value<T>(forKey key: TunnelEnvironmentKey<T>) -> T? where T: Decodable {
|
public func value<T>(forKey key: TunnelEnvironmentKey<T>) -> T? where T: Decodable {
|
||||||
|
@ -54,10 +56,12 @@ public final class ExtendedTunnel: ObservableObject {
|
||||||
public init(
|
public init(
|
||||||
tunnel: Tunnel,
|
tunnel: Tunnel,
|
||||||
environment: TunnelEnvironment,
|
environment: TunnelEnvironment,
|
||||||
|
processor: ProfileProcessor? = nil,
|
||||||
interval: TimeInterval
|
interval: TimeInterval
|
||||||
) {
|
) {
|
||||||
self.tunnel = tunnel
|
self.tunnel = tunnel
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
self.processor = processor
|
||||||
self.interval = interval
|
self.interval = interval
|
||||||
subscriptions = []
|
subscriptions = []
|
||||||
}
|
}
|
||||||
|
@ -138,14 +142,14 @@ extension ExtendedTunnel {
|
||||||
try await tunnel.prepare(purge: purge)
|
try await tunnel.prepare(purge: purge)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func install(_ profile: Profile, processor: ProfileProcessor) async throws {
|
public func install(_ profile: Profile) async throws {
|
||||||
let newProfile = try processor.processed(profile)
|
let newProfile = try processedProfile(profile)
|
||||||
try await tunnel.install(newProfile, connect: false, title: processor.title)
|
try await tunnel.install(newProfile, connect: false, title: processedTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func connect(with profile: Profile, processor: ProfileProcessor) async throws {
|
public func connect(with profile: Profile) async throws {
|
||||||
let newProfile = try processor.processed(profile)
|
let newProfile = try processedProfile(profile)
|
||||||
try await tunnel.install(newProfile, connect: true, title: processor.title)
|
try await tunnel.install(newProfile, connect: true, title: processedTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func disconnect() async throws {
|
public func disconnect() async throws {
|
||||||
|
@ -166,3 +170,19 @@ extension ExtendedTunnel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension ExtendedTunnel {
|
||||||
|
var processedTitle: (Profile) -> String {
|
||||||
|
if let processor {
|
||||||
|
return processor.title
|
||||||
|
}
|
||||||
|
return \.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func processedProfile(_ profile: Profile) throws -> Profile {
|
||||||
|
if let processor {
|
||||||
|
return try processor.willConnect(profile)
|
||||||
|
}
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ public final class ProfileManager: ObservableObject {
|
||||||
|
|
||||||
private let deletingRemotely: Bool
|
private let deletingRemotely: Bool
|
||||||
|
|
||||||
private let isIncluded: ((Profile) -> Bool)?
|
private let processor: ProfileProcessor?
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
private var profiles: [Profile]
|
private var profiles: [Profile]
|
||||||
|
@ -68,7 +68,7 @@ public final class ProfileManager: ObservableObject {
|
||||||
backupRepository = nil
|
backupRepository = nil
|
||||||
remoteRepository = nil
|
remoteRepository = nil
|
||||||
deletingRemotely = false
|
deletingRemotely = false
|
||||||
isIncluded = nil
|
processor = nil
|
||||||
self.profiles = []
|
self.profiles = []
|
||||||
allProfiles = profiles.reduce(into: [:]) {
|
allProfiles = profiles.reduce(into: [:]) {
|
||||||
$0[$1.id] = $1
|
$0[$1.id] = $1
|
||||||
|
@ -85,14 +85,14 @@ public final class ProfileManager: ObservableObject {
|
||||||
backupRepository: (any ProfileRepository)? = nil,
|
backupRepository: (any ProfileRepository)? = nil,
|
||||||
remoteRepository: (any ProfileRepository)?,
|
remoteRepository: (any ProfileRepository)?,
|
||||||
deletingRemotely: Bool = false,
|
deletingRemotely: Bool = false,
|
||||||
isIncluded: ((Profile) -> Bool)? = nil
|
processor: ProfileProcessor? = nil
|
||||||
) {
|
) {
|
||||||
precondition(!deletingRemotely || remoteRepository != nil, "deletingRemotely requires a non-nil remoteRepository")
|
precondition(!deletingRemotely || remoteRepository != nil, "deletingRemotely requires a non-nil remoteRepository")
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
self.backupRepository = backupRepository
|
self.backupRepository = backupRepository
|
||||||
self.remoteRepository = remoteRepository
|
self.remoteRepository = remoteRepository
|
||||||
self.deletingRemotely = deletingRemotely
|
self.deletingRemotely = deletingRemotely
|
||||||
self.isIncluded = isIncluded
|
self.processor = processor
|
||||||
profiles = []
|
profiles = []
|
||||||
allProfiles = [:]
|
allProfiles = [:]
|
||||||
allRemoteProfiles = [:]
|
allRemoteProfiles = [:]
|
||||||
|
@ -134,6 +134,9 @@ extension ProfileManager {
|
||||||
let profile: Profile
|
let profile: Profile
|
||||||
if force {
|
if force {
|
||||||
var builder = originalProfile.builder()
|
var builder = originalProfile.builder()
|
||||||
|
if let processor {
|
||||||
|
builder = try processor.willSave(builder)
|
||||||
|
}
|
||||||
builder.attributes.lastUpdate = Date()
|
builder.attributes.lastUpdate = Date()
|
||||||
builder.attributes.fingerprint = UUID()
|
builder.attributes.fingerprint = UUID()
|
||||||
profile = try builder.tryBuild()
|
profile = try builder.tryBuild()
|
||||||
|
@ -319,7 +322,7 @@ private extension ProfileManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// should not be imported at all, but you never know
|
// should not be imported at all, but you never know
|
||||||
if let isIncluded {
|
if let isIncluded = processor?.isIncluded {
|
||||||
let idsToRemove: [Profile.ID] = allProfiles
|
let idsToRemove: [Profile.ID] = allProfiles
|
||||||
.filter {
|
.filter {
|
||||||
!isIncluded($0.value)
|
!isIncluded($0.value)
|
||||||
|
@ -372,7 +375,7 @@ private extension ProfileManager {
|
||||||
}
|
}
|
||||||
for remoteProfile in profilesToImport {
|
for remoteProfile in profilesToImport {
|
||||||
do {
|
do {
|
||||||
guard isIncluded?(remoteProfile) ?? true else {
|
guard processor?.isIncluded(remoteProfile) ?? true else {
|
||||||
pp_log(.app, .info, "\tWill delete non-included remote profile \(remoteProfile.id)")
|
pp_log(.app, .info, "\tWill delete non-included remote profile \(remoteProfile.id)")
|
||||||
idsToRemove.append(remoteProfile.id)
|
idsToRemove.append(remoteProfile.id)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -26,17 +26,40 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
@MainActor
|
|
||||||
public final class ProfileProcessor: ObservableObject {
|
public final class ProfileProcessor: ObservableObject {
|
||||||
|
private let iapManager: IAPManager
|
||||||
|
|
||||||
public let title: (Profile) -> String
|
public let title: (Profile) -> String
|
||||||
|
|
||||||
public let processed: (Profile) throws -> Profile
|
private let _isIncluded: (IAPManager, Profile) -> Bool
|
||||||
|
|
||||||
|
private let _willSave: (IAPManager, Profile.Builder) throws -> Profile.Builder
|
||||||
|
|
||||||
|
private let _willConnect: (IAPManager, Profile) throws -> Profile
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
iapManager: IAPManager,
|
||||||
title: @escaping (Profile) -> String,
|
title: @escaping (Profile) -> String,
|
||||||
processed: @escaping (Profile) throws -> Profile
|
isIncluded: @escaping (IAPManager, Profile) -> Bool,
|
||||||
|
willSave: @escaping (IAPManager, Profile.Builder) throws -> Profile.Builder,
|
||||||
|
willConnect: @escaping (IAPManager, Profile) throws -> Profile
|
||||||
) {
|
) {
|
||||||
|
self.iapManager = iapManager
|
||||||
self.title = title
|
self.title = title
|
||||||
self.processed = processed
|
_isIncluded = isIncluded
|
||||||
|
_willSave = willSave
|
||||||
|
_willConnect = willConnect
|
||||||
|
}
|
||||||
|
|
||||||
|
public func isIncluded(_ profile: Profile) -> Bool {
|
||||||
|
_isIncluded(iapManager, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func willSave(_ builder: Profile.Builder) throws -> Profile.Builder {
|
||||||
|
try _willSave(iapManager, builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func willConnect(_ profile: Profile) throws -> Profile {
|
||||||
|
try _willConnect(iapManager, profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
public enum AppError {
|
public enum AppError: Error {
|
||||||
case emptyProfileName
|
case emptyProfileName
|
||||||
|
|
||||||
case malformedModule(any ModuleBuilder, error: Error)
|
case malformedModule(any ModuleBuilder, error: Error)
|
|
@ -53,6 +53,8 @@ public struct ProfileAttributes: Hashable, Codable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - ProfileUserInfoTransformable
|
||||||
|
|
||||||
// FIXME: #570, test user info encoding/decoding with JSONSerialization
|
// FIXME: #570, test user info encoding/decoding with JSONSerialization
|
||||||
extension ProfileAttributes: ProfileUserInfoTransformable {
|
extension ProfileAttributes: ProfileUserInfoTransformable {
|
||||||
public var userInfo: [String: AnyHashable]? {
|
public var userInfo: [String: AnyHashable]? {
|
||||||
|
|
|
@ -25,4 +25,4 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public typealias BuildProducts<ProductType> = (_ at: Int) -> Set<ProductType> where ProductType: Hashable
|
public typealias BuildProducts<ProductType> = @Sendable (_ at: Int) -> Set<ProductType> where ProductType: Hashable
|
||||||
|
|
|
@ -35,42 +35,26 @@ public final class AppContext: ObservableObject {
|
||||||
|
|
||||||
public let profileManager: ProfileManager
|
public let profileManager: ProfileManager
|
||||||
|
|
||||||
public let profileProcessor: ProfileProcessor
|
|
||||||
|
|
||||||
public let tunnel: ExtendedTunnel
|
public let tunnel: ExtendedTunnel
|
||||||
|
|
||||||
public let tunnelEnvironment: TunnelEnvironment
|
|
||||||
|
|
||||||
public let registry: Registry
|
public let registry: Registry
|
||||||
|
|
||||||
public let providerManager: ProviderManager
|
public let providerManager: ProviderManager
|
||||||
|
|
||||||
private let constants: Constants
|
|
||||||
|
|
||||||
private var subscriptions: Set<AnyCancellable>
|
private var subscriptions: Set<AnyCancellable>
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
iapManager: IAPManager,
|
iapManager: IAPManager,
|
||||||
profileManager: ProfileManager,
|
profileManager: ProfileManager,
|
||||||
profileProcessor: ProfileProcessor,
|
tunnel: ExtendedTunnel,
|
||||||
tunnel: Tunnel,
|
|
||||||
tunnelEnvironment: TunnelEnvironment,
|
|
||||||
registry: Registry,
|
registry: Registry,
|
||||||
providerManager: ProviderManager,
|
providerManager: ProviderManager
|
||||||
constants: Constants
|
|
||||||
) {
|
) {
|
||||||
self.iapManager = iapManager
|
self.iapManager = iapManager
|
||||||
self.profileManager = profileManager
|
self.profileManager = profileManager
|
||||||
self.profileProcessor = profileProcessor
|
self.tunnel = tunnel
|
||||||
self.tunnelEnvironment = tunnelEnvironment
|
|
||||||
self.tunnel = ExtendedTunnel(
|
|
||||||
tunnel: tunnel,
|
|
||||||
environment: tunnelEnvironment,
|
|
||||||
interval: constants.tunnel.refreshInterval
|
|
||||||
)
|
|
||||||
self.registry = registry
|
self.registry = registry
|
||||||
self.providerManager = providerManager
|
self.providerManager = providerManager
|
||||||
self.constants = constants
|
|
||||||
subscriptions = []
|
subscriptions = []
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
@ -115,7 +99,7 @@ private extension AppContext {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
try await tunnel.connect(with: profile, processor: profileProcessor)
|
try await tunnel.connect(with: profile)
|
||||||
} catch {
|
} catch {
|
||||||
try await tunnel.disconnect()
|
try await tunnel.disconnect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ extension View {
|
||||||
public func withEnvironment(from context: AppContext, theme: Theme) -> some View {
|
public func withEnvironment(from context: AppContext, theme: Theme) -> some View {
|
||||||
environmentObject(theme)
|
environmentObject(theme)
|
||||||
.environmentObject(context.iapManager)
|
.environmentObject(context.iapManager)
|
||||||
.environmentObject(context.profileProcessor)
|
|
||||||
.environmentObject(context.providerManager)
|
.environmentObject(context.providerManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,8 +98,14 @@ extension PassepartoutError: LocalizedError {
|
||||||
|
|
||||||
// MARK: - Tunnel side
|
// MARK: - Tunnel side
|
||||||
|
|
||||||
extension PassepartoutError.Code: LocalizableEntity {
|
extension PassepartoutError.Code: StyledLocalizableEntity {
|
||||||
public var localizedDescription: String {
|
public enum Style {
|
||||||
|
case tunnel
|
||||||
|
}
|
||||||
|
|
||||||
|
public func localizedDescription(style: Style) -> String {
|
||||||
|
switch style {
|
||||||
|
case .tunnel:
|
||||||
let V = Strings.Errors.Tunnel.self
|
let V = Strings.Errors.Tunnel.self
|
||||||
switch self {
|
switch self {
|
||||||
case .authentication:
|
case .authentication:
|
||||||
|
@ -131,3 +137,4 @@ extension PassepartoutError.Code: LocalizableEntity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CommonLibrary
|
||||||
import CommonUtils
|
import CommonUtils
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
//
|
||||||
|
// AppContext+Mock.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 6/22/24.
|
||||||
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CommonLibrary
|
||||||
|
import CommonUtils
|
||||||
|
import Foundation
|
||||||
|
import PassepartoutKit
|
||||||
|
|
||||||
|
extension AppContext {
|
||||||
|
public static let mock: AppContext = .mock(withRegistry: Registry())
|
||||||
|
|
||||||
|
public static func mock(withRegistry registry: Registry) -> AppContext {
|
||||||
|
let iapManager = IAPManager(
|
||||||
|
customUserLevel: nil,
|
||||||
|
receiptReader: MockAppReceiptReader(),
|
||||||
|
unrestrictedFeatures: [
|
||||||
|
.interactiveLogin,
|
||||||
|
.onDemand,
|
||||||
|
.sharing
|
||||||
|
],
|
||||||
|
productsAtBuild: { _ in
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let processor = ProfileProcessor(
|
||||||
|
iapManager: iapManager,
|
||||||
|
title: {
|
||||||
|
"Passepartout.Mock: \($0.name)"
|
||||||
|
},
|
||||||
|
isIncluded: { _, _ in
|
||||||
|
true
|
||||||
|
},
|
||||||
|
willSave: { _, builder in
|
||||||
|
builder
|
||||||
|
},
|
||||||
|
willConnect: { _, profile in
|
||||||
|
try profile.withProviderModules()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let profileManager = {
|
||||||
|
let profiles: [Profile] = (0..<20)
|
||||||
|
.reduce(into: []) { list, _ in
|
||||||
|
list.append(.newMockProfile())
|
||||||
|
}
|
||||||
|
return ProfileManager(profiles: profiles)
|
||||||
|
}()
|
||||||
|
let tunnelEnvironment = InMemoryEnvironment()
|
||||||
|
let tunnel = ExtendedTunnel(
|
||||||
|
tunnel: Tunnel(strategy: FakeTunnelStrategy(environment: tunnelEnvironment)),
|
||||||
|
environment: tunnelEnvironment,
|
||||||
|
processor: processor,
|
||||||
|
interval: Constants.shared.tunnel.refreshInterval
|
||||||
|
)
|
||||||
|
let providerManager = ProviderManager(
|
||||||
|
repository: InMemoryProviderRepository()
|
||||||
|
)
|
||||||
|
return AppContext(
|
||||||
|
iapManager: iapManager,
|
||||||
|
profileManager: profileManager,
|
||||||
|
tunnel: tunnel,
|
||||||
|
registry: registry,
|
||||||
|
providerManager: providerManager
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Shortcuts
|
||||||
|
|
||||||
|
extension IAPManager {
|
||||||
|
public static var mock: IAPManager {
|
||||||
|
AppContext.mock.iapManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileManager {
|
||||||
|
public static var mock: ProfileManager {
|
||||||
|
AppContext.mock.profileManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ExtendedTunnel {
|
||||||
|
public static var mock: ExtendedTunnel {
|
||||||
|
AppContext.mock.tunnel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProviderManager {
|
||||||
|
public static var mock: ProviderManager {
|
||||||
|
AppContext.mock.providerManager
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
//
|
//
|
||||||
// Mock.swift
|
// Profile+Mock.swift
|
||||||
// Passepartout
|
// Passepartout
|
||||||
//
|
//
|
||||||
// Created by Davide De Rosa on 6/22/24.
|
// Created by Davide De Rosa on 11/4/24.
|
||||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
//
|
//
|
||||||
// https://github.com/passepartoutvpn
|
// https://github.com/passepartoutvpn
|
||||||
|
@ -23,85 +23,9 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
|
||||||
import CommonLibrary
|
|
||||||
import CommonUtils
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
extension AppContext {
|
|
||||||
public static let mock: AppContext = .mock(withRegistry: Registry())
|
|
||||||
|
|
||||||
public static func mock(withRegistry registry: Registry) -> AppContext {
|
|
||||||
let env = InMemoryEnvironment()
|
|
||||||
return AppContext(
|
|
||||||
iapManager: IAPManager(
|
|
||||||
customUserLevel: nil,
|
|
||||||
receiptReader: MockAppReceiptReader(),
|
|
||||||
unrestrictedFeatures: [
|
|
||||||
.interactiveLogin,
|
|
||||||
.onDemand,
|
|
||||||
.sharing
|
|
||||||
],
|
|
||||||
productsAtBuild: { _ in
|
|
||||||
[]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
profileManager: {
|
|
||||||
let profiles: [Profile] = (0..<20)
|
|
||||||
.reduce(into: []) { list, _ in
|
|
||||||
list.append(.newMockProfile())
|
|
||||||
}
|
|
||||||
return ProfileManager(profiles: profiles)
|
|
||||||
}(),
|
|
||||||
profileProcessor: ProfileProcessor {
|
|
||||||
"Passepartout.Mock: \($0.name)"
|
|
||||||
} processed: {
|
|
||||||
try $0.withProviderModules()
|
|
||||||
},
|
|
||||||
tunnel: Tunnel(strategy: FakeTunnelStrategy(environment: env)),
|
|
||||||
tunnelEnvironment: env,
|
|
||||||
registry: registry,
|
|
||||||
providerManager: ProviderManager(
|
|
||||||
repository: InMemoryProviderRepository()
|
|
||||||
),
|
|
||||||
constants: .shared
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension IAPManager {
|
|
||||||
public static var mock: IAPManager {
|
|
||||||
AppContext.mock.iapManager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileManager {
|
|
||||||
public static var mock: ProfileManager {
|
|
||||||
AppContext.mock.profileManager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileProcessor {
|
|
||||||
public static var mock: ProfileProcessor {
|
|
||||||
AppContext.mock.profileProcessor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ExtendedTunnel {
|
|
||||||
public static var mock: ExtendedTunnel {
|
|
||||||
AppContext.mock.tunnel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProviderManager {
|
|
||||||
public static var mock: ProviderManager {
|
|
||||||
AppContext.mock.providerManager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Profile
|
|
||||||
|
|
||||||
extension Profile {
|
extension Profile {
|
||||||
public static let mock: Profile = {
|
public static let mock: Profile = {
|
||||||
var profile = Profile.Builder()
|
var profile = Profile.Builder()
|
|
@ -48,7 +48,7 @@ public struct ConnectionStatusText: View, ThemeProviding {
|
||||||
private extension ConnectionStatusText {
|
private extension ConnectionStatusText {
|
||||||
var statusDescription: String {
|
var statusDescription: String {
|
||||||
if let lastErrorCode = tunnel.lastErrorCode {
|
if let lastErrorCode = tunnel.lastErrorCode {
|
||||||
return lastErrorCode.localizedDescription
|
return lastErrorCode.localizedDescription(style: .tunnel)
|
||||||
}
|
}
|
||||||
let status = tunnel.connectionStatus
|
let status = tunnel.connectionStatus
|
||||||
switch status {
|
switch status {
|
||||||
|
@ -61,9 +61,11 @@ private extension ConnectionStatusText {
|
||||||
|
|
||||||
case .inactive:
|
case .inactive:
|
||||||
var desc = status.localizedDescription
|
var desc = status.localizedDescription
|
||||||
if tunnel.currentProfile?.onDemand ?? false {
|
if let profile = tunnel.currentProfile {
|
||||||
|
if profile.onDemand {
|
||||||
desc += Strings.Ui.ConnectionStatus.onDemandSuffix
|
desc += Strings.Ui.ConnectionStatus.onDemandSuffix
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return desc
|
return desc
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -76,7 +78,7 @@ private extension ConnectionStatusText {
|
||||||
#Preview("Connected") {
|
#Preview("Connected") {
|
||||||
ConnectionStatusText(tunnel: .mock)
|
ConnectionStatusText(tunnel: .mock)
|
||||||
.task {
|
.task {
|
||||||
try? await ExtendedTunnel.mock.connect(with: .mock, processor: .mock)
|
try? await ExtendedTunnel.mock.connect(with: .mock)
|
||||||
}
|
}
|
||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
.withMockEnvironment()
|
.withMockEnvironment()
|
||||||
|
@ -95,7 +97,7 @@ private extension ConnectionStatusText {
|
||||||
}
|
}
|
||||||
return ConnectionStatusText(tunnel: .mock)
|
return ConnectionStatusText(tunnel: .mock)
|
||||||
.task {
|
.task {
|
||||||
try? await ExtendedTunnel.mock.connect(with: profile, processor: .mock)
|
try? await ExtendedTunnel.mock.connect(with: profile)
|
||||||
}
|
}
|
||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
.withMockEnvironment()
|
.withMockEnvironment()
|
||||||
|
|
|
@ -36,9 +36,6 @@ public struct TunnelToggleButton<Label>: View, ThemeProviding where Label: View
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var iapManager: IAPManager
|
private var iapManager: IAPManager
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var profileProcessor: ProfileProcessor
|
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
private var tunnel: ExtendedTunnel
|
private var tunnel: ExtendedTunnel
|
||||||
|
|
||||||
|
@ -128,12 +125,12 @@ private extension TunnelToggleButton {
|
||||||
do {
|
do {
|
||||||
if isInstalled {
|
if isInstalled {
|
||||||
if canConnect {
|
if canConnect {
|
||||||
try await tunnel.connect(with: profile, processor: profileProcessor)
|
try await tunnel.connect(with: profile)
|
||||||
} else {
|
} else {
|
||||||
try await tunnel.disconnect()
|
try await tunnel.disconnect()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try await tunnel.connect(with: profile, processor: profileProcessor)
|
try await tunnel.connect(with: profile)
|
||||||
}
|
}
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
//
|
//
|
||||||
|
|
|
@ -33,22 +33,56 @@ import PassepartoutKit
|
||||||
import UILibrary
|
import UILibrary
|
||||||
|
|
||||||
extension AppContext {
|
extension AppContext {
|
||||||
static let shared = AppContext(
|
static let shared: AppContext = {
|
||||||
iapManager: .shared,
|
let tunnelEnvironment: TunnelEnvironment = .shared
|
||||||
profileManager: .shared,
|
let registry: Registry = .shared
|
||||||
profileProcessor: .shared,
|
|
||||||
tunnel: .shared,
|
let iapManager = IAPManager(
|
||||||
tunnelEnvironment: .shared,
|
customUserLevel: Configuration.IAPManager.customUserLevel,
|
||||||
registry: .shared,
|
receiptReader: KvittoReceiptReader(),
|
||||||
providerManager: .shared,
|
// FIXME: #662, omit unrestrictedFeatures on release!
|
||||||
constants: .shared
|
unrestrictedFeatures: [.interactiveLogin, .sharing],
|
||||||
|
productsAtBuild: Configuration.IAPManager.productsAtBuild
|
||||||
)
|
)
|
||||||
|
let processor = ProfileProcessor(
|
||||||
|
iapManager: iapManager,
|
||||||
|
title: {
|
||||||
|
Configuration.ProfileManager.sharedTitle($0)
|
||||||
|
},
|
||||||
|
isIncluded: { _, profile in
|
||||||
|
Configuration.ProfileManager.isProfileIncluded(profile)
|
||||||
|
},
|
||||||
|
willSave: { _, builder in
|
||||||
|
builder
|
||||||
|
},
|
||||||
|
willConnect: { iap, profile in
|
||||||
|
var builder = profile.builder()
|
||||||
|
|
||||||
|
// suppress on-demand rules if not eligible
|
||||||
|
if !iap.isEligible(for: .onDemand) {
|
||||||
|
pp_log(.app, .notice, "Suppress on-demand rules, not eligible")
|
||||||
|
|
||||||
|
if let onDemandModuleIndex = builder.modules.firstIndex(where: { $0 is OnDemandModule }),
|
||||||
|
let onDemandModule = builder.modules[onDemandModuleIndex] as? OnDemandModule {
|
||||||
|
|
||||||
|
var onDemandBuilder = onDemandModule.builder()
|
||||||
|
onDemandBuilder.policy = .any
|
||||||
|
builder.modules[onDemandModuleIndex] = onDemandBuilder.tryBuild()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -
|
// validate provider modules
|
||||||
|
let profile = try builder.tryBuild()
|
||||||
extension ProfileManager {
|
do {
|
||||||
static let shared: ProfileManager = {
|
_ = try profile.withProviderModules()
|
||||||
|
return profile
|
||||||
|
} catch {
|
||||||
|
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let profileManager: ProfileManager = {
|
||||||
let remoteStore = CoreDataPersistentStore(
|
let remoteStore = CoreDataPersistentStore(
|
||||||
logger: .default,
|
logger: .default,
|
||||||
containerName: Constants.shared.containers.remote,
|
containerName: Constants.shared.containers.remote,
|
||||||
|
@ -65,41 +99,52 @@ extension ProfileManager {
|
||||||
pp_log(.app, .error, "Unable to decode remote result: \(error)")
|
pp_log(.app, .error, "Unable to decode remote result: \(error)")
|
||||||
return .ignore
|
return .ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
return ProfileManager(
|
return ProfileManager(
|
||||||
repository: mainProfileRepository,
|
repository: Configuration.ProfileManager.mainProfileRepository,
|
||||||
backupRepository: backupProfileRepository,
|
backupRepository: Configuration.ProfileManager.backupProfileRepository,
|
||||||
remoteRepository: remoteRepository,
|
remoteRepository: remoteRepository,
|
||||||
deletingRemotely: deletingRemotely,
|
deletingRemotely: Configuration.ProfileManager.deletingRemotely,
|
||||||
isIncluded: isProfileIncluded
|
processor: processor
|
||||||
)
|
)
|
||||||
}()
|
}()
|
||||||
|
let tunnel = ExtendedTunnel(
|
||||||
#if os(tvOS)
|
tunnel: Tunnel(strategy: Configuration.ExtendedTunnel.strategy),
|
||||||
private static let deletingRemotely = true
|
environment: tunnelEnvironment,
|
||||||
|
processor: processor,
|
||||||
private static let isProfileIncluded: (Profile) -> Bool = {
|
interval: Constants.shared.tunnel.refreshInterval
|
||||||
$0.attributes.isAvailableForTV == true
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
private static let deletingRemotely = false
|
|
||||||
|
|
||||||
private static let isProfileIncluded: ((Profile) -> Bool)? = nil
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
extension IAPManager {
|
|
||||||
static let shared = IAPManager(
|
|
||||||
customUserLevel: customUserLevel,
|
|
||||||
receiptReader: KvittoReceiptReader(),
|
|
||||||
// FIXME: #662, omit unrestrictedFeatures on release!
|
|
||||||
unrestrictedFeatures: [.interactiveLogin, .sharing],
|
|
||||||
productsAtBuild: productsAtBuild
|
|
||||||
)
|
)
|
||||||
|
let providerManager: ProviderManager = {
|
||||||
|
let store = CoreDataPersistentStore(
|
||||||
|
logger: .default,
|
||||||
|
containerName: Constants.shared.containers.providers,
|
||||||
|
model: AppData.cdProvidersModel,
|
||||||
|
cloudKitIdentifier: nil,
|
||||||
|
author: nil
|
||||||
|
)
|
||||||
|
let repository = AppData.cdProviderRepositoryV3(
|
||||||
|
context: store.context,
|
||||||
|
backgroundContext: store.backgroundContext
|
||||||
|
)
|
||||||
|
return ProviderManager(repository: repository)
|
||||||
|
}()
|
||||||
|
return AppContext(
|
||||||
|
iapManager: iapManager,
|
||||||
|
profileManager: profileManager,
|
||||||
|
tunnel: tunnel,
|
||||||
|
registry: registry,
|
||||||
|
providerManager: providerManager
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
private static var customUserLevel: AppUserLevel? {
|
// MARK: - Configuration
|
||||||
|
|
||||||
|
private enum Configuration {
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Configuration {
|
||||||
|
enum IAPManager {
|
||||||
|
static var customUserLevel: AppUserLevel? {
|
||||||
if let envString = ProcessInfo.processInfo.environment["CUSTOM_USER_LEVEL"],
|
if let envString = ProcessInfo.processInfo.environment["CUSTOM_USER_LEVEL"],
|
||||||
let envValue = Int(envString),
|
let envValue = Int(envString),
|
||||||
let testAppType = AppUserLevel(rawValue: envValue) {
|
let testAppType = AppUserLevel(rawValue: envValue) {
|
||||||
|
@ -114,7 +159,7 @@ extension IAPManager {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let productsAtBuild: BuildProducts<AppProduct> = {
|
static let productsAtBuild: BuildProducts<AppProduct> = {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if $0 <= 2016 {
|
if $0 <= 2016 {
|
||||||
return [.Full.iOS]
|
return [.Full.iOS]
|
||||||
|
@ -132,48 +177,42 @@ extension IAPManager {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileProcessor {
|
|
||||||
static let shared = ProfileProcessor {
|
|
||||||
ProfileManager.sharedTitle($0)
|
|
||||||
} processed: { profile in
|
|
||||||
var builder = profile.builder()
|
|
||||||
|
|
||||||
// suppress on-demand rules if not eligible
|
|
||||||
if !IAPManager.shared.isEligible(for: .onDemand) {
|
|
||||||
pp_log(.app, .notice, "Suppress on-demand rules, not eligible")
|
|
||||||
|
|
||||||
if let onDemandModuleIndex = builder.modules.firstIndex(where: { $0 is OnDemandModule }),
|
|
||||||
let onDemandModule = builder.modules[onDemandModuleIndex] as? OnDemandModule {
|
|
||||||
|
|
||||||
var onDemandBuilder = onDemandModule.builder()
|
|
||||||
onDemandBuilder.policy = .any
|
|
||||||
builder.modules[onDemandModuleIndex] = onDemandBuilder.tryBuild()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let profile = try builder.tryBuild()
|
extension Configuration {
|
||||||
do {
|
enum ProfileManager {
|
||||||
_ = try profile.withProviderModules()
|
static let sharedTitle: @Sendable (Profile) -> String = {
|
||||||
return profile
|
String(format: Constants.shared.tunnel.profileTitleFormat, $0.name)
|
||||||
} catch {
|
|
||||||
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -
|
#if os(tvOS)
|
||||||
|
static let deletingRemotely = true
|
||||||
|
|
||||||
|
static let isProfileIncluded: @Sendable (Profile) -> Bool = {
|
||||||
|
$0.attributes.isAvailableForTV == true
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static let deletingRemotely = false
|
||||||
|
|
||||||
|
static let isProfileIncluded: (Profile) -> Bool = { _ in
|
||||||
|
true
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if targetEnvironment(simulator)
|
#if targetEnvironment(simulator)
|
||||||
|
|
||||||
extension Tunnel {
|
extension Configuration {
|
||||||
static let shared = Tunnel(
|
enum ExtendedTunnel {
|
||||||
strategy: FakeTunnelStrategy(environment: .shared, dataCountInterval: 1000)
|
static var strategy: TunnelObservableStrategy {
|
||||||
)
|
FakeTunnelStrategy(environment: .shared, dataCountInterval: 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ProfileManager {
|
@MainActor
|
||||||
|
extension Configuration.ProfileManager {
|
||||||
static var mainProfileRepository: ProfileRepository {
|
static var mainProfileRepository: ProfileRepository {
|
||||||
coreDataProfileRepository
|
coreDataProfileRepository
|
||||||
}
|
}
|
||||||
|
@ -185,13 +224,16 @@ private extension ProfileManager {
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
extension Tunnel {
|
extension Configuration {
|
||||||
static let shared = Tunnel(
|
enum ExtendedTunnel {
|
||||||
strategy: ProfileManager.neStrategy
|
static var strategy: TunnelObservableStrategy {
|
||||||
)
|
ProfileManager.neStrategy
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ProfileManager {
|
@MainActor
|
||||||
|
extension Configuration.ProfileManager {
|
||||||
static var mainProfileRepository: ProfileRepository {
|
static var mainProfileRepository: ProfileRepository {
|
||||||
neProfileRepository
|
neProfileRepository
|
||||||
}
|
}
|
||||||
|
@ -203,10 +245,11 @@ private extension ProfileManager {
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private extension ProfileManager {
|
@MainActor
|
||||||
|
extension Configuration.ProfileManager {
|
||||||
static let neProfileRepository: ProfileRepository = {
|
static let neProfileRepository: ProfileRepository = {
|
||||||
NEProfileRepository(repository: neStrategy) {
|
NEProfileRepository(repository: neStrategy) {
|
||||||
ProfileManager.sharedTitle($0)
|
sharedTitle($0)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -240,31 +283,6 @@ private extension ProfileManager {
|
||||||
|
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
|
||||||
extension ProviderManager {
|
|
||||||
static let shared: ProviderManager = {
|
|
||||||
let store = CoreDataPersistentStore(
|
|
||||||
logger: .default,
|
|
||||||
containerName: Constants.shared.containers.providers,
|
|
||||||
model: AppData.cdProvidersModel,
|
|
||||||
cloudKitIdentifier: nil,
|
|
||||||
author: nil
|
|
||||||
)
|
|
||||||
let repository = AppData.cdProviderRepositoryV3(
|
|
||||||
context: store.context,
|
|
||||||
backgroundContext: store.backgroundContext
|
|
||||||
)
|
|
||||||
return ProviderManager(repository: repository)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
private extension ProfileManager {
|
|
||||||
static let sharedTitle: (Profile) -> String = {
|
|
||||||
String(format: Constants.shared.tunnel.profileTitleFormat, $0.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CoreDataPersistentStoreLogger where Self == DefaultCoreDataPersistentStoreLogger {
|
extension CoreDataPersistentStoreLogger where Self == DefaultCoreDataPersistentStoreLogger {
|
||||||
static var `default`: CoreDataPersistentStoreLogger {
|
static var `default`: CoreDataPersistentStoreLogger {
|
||||||
DefaultCoreDataPersistentStoreLogger()
|
DefaultCoreDataPersistentStoreLogger()
|
||||||
|
|
Loading…
Reference in New Issue