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 {
|
||||
AppMenu(
|
||||
profileManager: context.profileManager,
|
||||
profileProcessor: context.profileProcessor,
|
||||
tunnel: context.tunnel
|
||||
)
|
||||
.withEnvironment(from: context, theme: theme)
|
||||
|
|
|
@ -68,13 +68,7 @@ private extension AppData {
|
|||
return nil
|
||||
}
|
||||
let profile = try registry.decodedProfile(from: encoded, with: coder)
|
||||
var builder = profile.builder()
|
||||
builder.attributes = ProfileAttributes(
|
||||
isAvailableForTV: cdEntity.isAvailableForTV?.boolValue ?? false,
|
||||
lastUpdate: cdEntity.lastUpdate,
|
||||
fingerprint: cdEntity.fingerprint
|
||||
)
|
||||
return try builder.tryBuild()
|
||||
return profile
|
||||
}
|
||||
|
||||
static func toMapper(
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import CommonUtils
|
||||
import SwiftUI
|
||||
|
||||
|
|
|
@ -30,9 +30,6 @@ import SwiftUI
|
|||
|
||||
struct ProviderEntitySelector: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var profileProcessor: ProfileProcessor
|
||||
|
||||
@ObservedObject
|
||||
var profileManager: ProfileManager
|
||||
|
||||
|
|
|
@ -30,9 +30,6 @@ import SwiftUI
|
|||
|
||||
struct TunnelRestartButton<Label>: View where Label: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var profileProcessor: ProfileProcessor
|
||||
|
||||
@ObservedObject
|
||||
var tunnel: ExtendedTunnel
|
||||
|
||||
|
@ -52,7 +49,7 @@ struct TunnelRestartButton<Label>: View where Label: View {
|
|||
}
|
||||
Task {
|
||||
do {
|
||||
try await tunnel.connect(with: profile, processor: profileProcessor)
|
||||
try await tunnel.connect(with: profile)
|
||||
} catch is CancellationError {
|
||||
//
|
||||
} catch {
|
||||
|
|
|
@ -35,18 +35,14 @@ public struct AppMenu: View {
|
|||
@ObservedObject
|
||||
private var profileManager: ProfileManager
|
||||
|
||||
@ObservedObject
|
||||
private var profileProcessor: ProfileProcessor
|
||||
|
||||
@ObservedObject
|
||||
private var tunnel: ExtendedTunnel
|
||||
|
||||
@StateObject
|
||||
private var model = Model()
|
||||
|
||||
public init(profileManager: ProfileManager, profileProcessor: ProfileProcessor, tunnel: ExtendedTunnel) {
|
||||
public init(profileManager: ProfileManager, tunnel: ExtendedTunnel) {
|
||||
self.profileManager = profileManager
|
||||
self.profileProcessor = profileProcessor
|
||||
self.tunnel = tunnel
|
||||
}
|
||||
|
||||
|
@ -96,7 +92,7 @@ private extension AppMenu {
|
|||
}
|
||||
do {
|
||||
if isOn {
|
||||
try await tunnel.connect(with: profile, processor: profileProcessor)
|
||||
try await tunnel.connect(with: profile)
|
||||
} else {
|
||||
try await tunnel.disconnect()
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
|
|
|
@ -33,6 +33,8 @@ public final class ExtendedTunnel: ObservableObject {
|
|||
|
||||
private let environment: TunnelEnvironment
|
||||
|
||||
private let processor: ProfileProcessor?
|
||||
|
||||
private let interval: TimeInterval
|
||||
|
||||
public func value<T>(forKey key: TunnelEnvironmentKey<T>) -> T? where T: Decodable {
|
||||
|
@ -54,10 +56,12 @@ public final class ExtendedTunnel: ObservableObject {
|
|||
public init(
|
||||
tunnel: Tunnel,
|
||||
environment: TunnelEnvironment,
|
||||
processor: ProfileProcessor? = nil,
|
||||
interval: TimeInterval
|
||||
) {
|
||||
self.tunnel = tunnel
|
||||
self.environment = environment
|
||||
self.processor = processor
|
||||
self.interval = interval
|
||||
subscriptions = []
|
||||
}
|
||||
|
@ -138,14 +142,14 @@ extension ExtendedTunnel {
|
|||
try await tunnel.prepare(purge: purge)
|
||||
}
|
||||
|
||||
public func install(_ profile: Profile, processor: ProfileProcessor) async throws {
|
||||
let newProfile = try processor.processed(profile)
|
||||
try await tunnel.install(newProfile, connect: false, title: processor.title)
|
||||
public func install(_ profile: Profile) async throws {
|
||||
let newProfile = try processedProfile(profile)
|
||||
try await tunnel.install(newProfile, connect: false, title: processedTitle)
|
||||
}
|
||||
|
||||
public func connect(with profile: Profile, processor: ProfileProcessor) async throws {
|
||||
let newProfile = try processor.processed(profile)
|
||||
try await tunnel.install(newProfile, connect: true, title: processor.title)
|
||||
public func connect(with profile: Profile) async throws {
|
||||
let newProfile = try processedProfile(profile)
|
||||
try await tunnel.install(newProfile, connect: true, title: processedTitle)
|
||||
}
|
||||
|
||||
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 isIncluded: ((Profile) -> Bool)?
|
||||
private let processor: ProfileProcessor?
|
||||
|
||||
@Published
|
||||
private var profiles: [Profile]
|
||||
|
@ -68,7 +68,7 @@ public final class ProfileManager: ObservableObject {
|
|||
backupRepository = nil
|
||||
remoteRepository = nil
|
||||
deletingRemotely = false
|
||||
isIncluded = nil
|
||||
processor = nil
|
||||
self.profiles = []
|
||||
allProfiles = profiles.reduce(into: [:]) {
|
||||
$0[$1.id] = $1
|
||||
|
@ -85,14 +85,14 @@ public final class ProfileManager: ObservableObject {
|
|||
backupRepository: (any ProfileRepository)? = nil,
|
||||
remoteRepository: (any ProfileRepository)?,
|
||||
deletingRemotely: Bool = false,
|
||||
isIncluded: ((Profile) -> Bool)? = nil
|
||||
processor: ProfileProcessor? = nil
|
||||
) {
|
||||
precondition(!deletingRemotely || remoteRepository != nil, "deletingRemotely requires a non-nil remoteRepository")
|
||||
self.repository = repository
|
||||
self.backupRepository = backupRepository
|
||||
self.remoteRepository = remoteRepository
|
||||
self.deletingRemotely = deletingRemotely
|
||||
self.isIncluded = isIncluded
|
||||
self.processor = processor
|
||||
profiles = []
|
||||
allProfiles = [:]
|
||||
allRemoteProfiles = [:]
|
||||
|
@ -134,6 +134,9 @@ extension ProfileManager {
|
|||
let profile: Profile
|
||||
if force {
|
||||
var builder = originalProfile.builder()
|
||||
if let processor {
|
||||
builder = try processor.willSave(builder)
|
||||
}
|
||||
builder.attributes.lastUpdate = Date()
|
||||
builder.attributes.fingerprint = UUID()
|
||||
profile = try builder.tryBuild()
|
||||
|
@ -319,7 +322,7 @@ private extension ProfileManager {
|
|||
}
|
||||
|
||||
// should not be imported at all, but you never know
|
||||
if let isIncluded {
|
||||
if let isIncluded = processor?.isIncluded {
|
||||
let idsToRemove: [Profile.ID] = allProfiles
|
||||
.filter {
|
||||
!isIncluded($0.value)
|
||||
|
@ -372,7 +375,7 @@ private extension ProfileManager {
|
|||
}
|
||||
for remoteProfile in profilesToImport {
|
||||
do {
|
||||
guard isIncluded?(remoteProfile) ?? true else {
|
||||
guard processor?.isIncluded(remoteProfile) ?? true else {
|
||||
pp_log(.app, .info, "\tWill delete non-included remote profile \(remoteProfile.id)")
|
||||
idsToRemove.append(remoteProfile.id)
|
||||
continue
|
||||
|
|
|
@ -26,17 +26,40 @@
|
|||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
@MainActor
|
||||
public final class ProfileProcessor: ObservableObject {
|
||||
private let iapManager: IAPManager
|
||||
|
||||
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(
|
||||
iapManager: IAPManager,
|
||||
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.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 PassepartoutKit
|
||||
|
||||
public enum AppError {
|
||||
public enum AppError: Error {
|
||||
case emptyProfileName
|
||||
|
||||
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
|
||||
extension ProfileAttributes: ProfileUserInfoTransformable {
|
||||
public var userInfo: [String: AnyHashable]? {
|
||||
|
|
|
@ -25,4 +25,4 @@
|
|||
|
||||
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 profileProcessor: ProfileProcessor
|
||||
|
||||
public let tunnel: ExtendedTunnel
|
||||
|
||||
public let tunnelEnvironment: TunnelEnvironment
|
||||
|
||||
public let registry: Registry
|
||||
|
||||
public let providerManager: ProviderManager
|
||||
|
||||
private let constants: Constants
|
||||
|
||||
private var subscriptions: Set<AnyCancellable>
|
||||
|
||||
public init(
|
||||
iapManager: IAPManager,
|
||||
profileManager: ProfileManager,
|
||||
profileProcessor: ProfileProcessor,
|
||||
tunnel: Tunnel,
|
||||
tunnelEnvironment: TunnelEnvironment,
|
||||
tunnel: ExtendedTunnel,
|
||||
registry: Registry,
|
||||
providerManager: ProviderManager,
|
||||
constants: Constants
|
||||
providerManager: ProviderManager
|
||||
) {
|
||||
self.iapManager = iapManager
|
||||
self.profileManager = profileManager
|
||||
self.profileProcessor = profileProcessor
|
||||
self.tunnelEnvironment = tunnelEnvironment
|
||||
self.tunnel = ExtendedTunnel(
|
||||
tunnel: tunnel,
|
||||
environment: tunnelEnvironment,
|
||||
interval: constants.tunnel.refreshInterval
|
||||
)
|
||||
self.tunnel = tunnel
|
||||
self.registry = registry
|
||||
self.providerManager = providerManager
|
||||
self.constants = constants
|
||||
subscriptions = []
|
||||
|
||||
Task {
|
||||
|
@ -115,7 +99,7 @@ private extension AppContext {
|
|||
return
|
||||
}
|
||||
do {
|
||||
try await tunnel.connect(with: profile, processor: profileProcessor)
|
||||
try await tunnel.connect(with: profile)
|
||||
} catch {
|
||||
try await tunnel.disconnect()
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ extension View {
|
|||
public func withEnvironment(from context: AppContext, theme: Theme) -> some View {
|
||||
environmentObject(theme)
|
||||
.environmentObject(context.iapManager)
|
||||
.environmentObject(context.profileProcessor)
|
||||
.environmentObject(context.providerManager)
|
||||
}
|
||||
|
||||
|
|
|
@ -98,36 +98,43 @@ extension PassepartoutError: LocalizedError {
|
|||
|
||||
// MARK: - Tunnel side
|
||||
|
||||
extension PassepartoutError.Code: LocalizableEntity {
|
||||
public var localizedDescription: String {
|
||||
let V = Strings.Errors.Tunnel.self
|
||||
switch self {
|
||||
case .authentication:
|
||||
return V.auth
|
||||
extension PassepartoutError.Code: StyledLocalizableEntity {
|
||||
public enum Style {
|
||||
case tunnel
|
||||
}
|
||||
|
||||
case .crypto:
|
||||
return V.encryption
|
||||
public func localizedDescription(style: Style) -> String {
|
||||
switch style {
|
||||
case .tunnel:
|
||||
let V = Strings.Errors.Tunnel.self
|
||||
switch self {
|
||||
case .authentication:
|
||||
return V.auth
|
||||
|
||||
case .dnsFailure:
|
||||
return V.dns
|
||||
case .crypto:
|
||||
return V.encryption
|
||||
|
||||
case .timeout:
|
||||
return V.timeout
|
||||
case .dnsFailure:
|
||||
return V.dns
|
||||
|
||||
case .OpenVPN.compressionMismatch:
|
||||
return V.compression
|
||||
case .timeout:
|
||||
return V.timeout
|
||||
|
||||
case .OpenVPN.noRouting:
|
||||
return V.routing
|
||||
case .OpenVPN.compressionMismatch:
|
||||
return V.compression
|
||||
|
||||
case .OpenVPN.serverShutdown:
|
||||
return V.shutdown
|
||||
case .OpenVPN.noRouting:
|
||||
return V.routing
|
||||
|
||||
case .OpenVPN.tlsFailure:
|
||||
return V.tls
|
||||
case .OpenVPN.serverShutdown:
|
||||
return V.shutdown
|
||||
|
||||
default:
|
||||
return V.generic
|
||||
case .OpenVPN.tlsFailure:
|
||||
return V.tls
|
||||
|
||||
default:
|
||||
return V.generic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import CommonUtils
|
||||
import PassepartoutKit
|
||||
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
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
|
@ -23,85 +23,9 @@
|
|||
// 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 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 {
|
||||
public static let mock: Profile = {
|
||||
var profile = Profile.Builder()
|
|
@ -48,7 +48,7 @@ public struct ConnectionStatusText: View, ThemeProviding {
|
|||
private extension ConnectionStatusText {
|
||||
var statusDescription: String {
|
||||
if let lastErrorCode = tunnel.lastErrorCode {
|
||||
return lastErrorCode.localizedDescription
|
||||
return lastErrorCode.localizedDescription(style: .tunnel)
|
||||
}
|
||||
let status = tunnel.connectionStatus
|
||||
switch status {
|
||||
|
@ -61,8 +61,10 @@ private extension ConnectionStatusText {
|
|||
|
||||
case .inactive:
|
||||
var desc = status.localizedDescription
|
||||
if tunnel.currentProfile?.onDemand ?? false {
|
||||
desc += Strings.Ui.ConnectionStatus.onDemandSuffix
|
||||
if let profile = tunnel.currentProfile {
|
||||
if profile.onDemand {
|
||||
desc += Strings.Ui.ConnectionStatus.onDemandSuffix
|
||||
}
|
||||
}
|
||||
return desc
|
||||
|
||||
|
@ -76,7 +78,7 @@ private extension ConnectionStatusText {
|
|||
#Preview("Connected") {
|
||||
ConnectionStatusText(tunnel: .mock)
|
||||
.task {
|
||||
try? await ExtendedTunnel.mock.connect(with: .mock, processor: .mock)
|
||||
try? await ExtendedTunnel.mock.connect(with: .mock)
|
||||
}
|
||||
.frame(width: 100, height: 100)
|
||||
.withMockEnvironment()
|
||||
|
@ -95,7 +97,7 @@ private extension ConnectionStatusText {
|
|||
}
|
||||
return ConnectionStatusText(tunnel: .mock)
|
||||
.task {
|
||||
try? await ExtendedTunnel.mock.connect(with: profile, processor: .mock)
|
||||
try? await ExtendedTunnel.mock.connect(with: profile)
|
||||
}
|
||||
.frame(width: 100, height: 100)
|
||||
.withMockEnvironment()
|
||||
|
|
|
@ -36,9 +36,6 @@ public struct TunnelToggleButton<Label>: View, ThemeProviding where Label: View
|
|||
@EnvironmentObject
|
||||
private var iapManager: IAPManager
|
||||
|
||||
@EnvironmentObject
|
||||
private var profileProcessor: ProfileProcessor
|
||||
|
||||
@ObservedObject
|
||||
private var tunnel: ExtendedTunnel
|
||||
|
||||
|
@ -128,12 +125,12 @@ private extension TunnelToggleButton {
|
|||
do {
|
||||
if isInstalled {
|
||||
if canConnect {
|
||||
try await tunnel.connect(with: profile, processor: profileProcessor)
|
||||
try await tunnel.connect(with: profile)
|
||||
} else {
|
||||
try await tunnel.disconnect()
|
||||
}
|
||||
} else {
|
||||
try await tunnel.connect(with: profile, processor: profileProcessor)
|
||||
try await tunnel.connect(with: profile)
|
||||
}
|
||||
} catch is CancellationError {
|
||||
//
|
||||
|
|
|
@ -33,147 +33,186 @@ import PassepartoutKit
|
|||
import UILibrary
|
||||
|
||||
extension AppContext {
|
||||
static let shared = AppContext(
|
||||
iapManager: .shared,
|
||||
profileManager: .shared,
|
||||
profileProcessor: .shared,
|
||||
tunnel: .shared,
|
||||
tunnelEnvironment: .shared,
|
||||
registry: .shared,
|
||||
providerManager: .shared,
|
||||
constants: .shared
|
||||
)
|
||||
}
|
||||
static let shared: AppContext = {
|
||||
let tunnelEnvironment: TunnelEnvironment = .shared
|
||||
let registry: Registry = .shared
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension ProfileManager {
|
||||
static let shared: ProfileManager = {
|
||||
let remoteStore = CoreDataPersistentStore(
|
||||
logger: .default,
|
||||
containerName: Constants.shared.containers.remote,
|
||||
model: AppData.cdProfilesModel,
|
||||
cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitId),
|
||||
author: nil
|
||||
let iapManager = IAPManager(
|
||||
customUserLevel: Configuration.IAPManager.customUserLevel,
|
||||
receiptReader: KvittoReceiptReader(),
|
||||
// FIXME: #662, omit unrestrictedFeatures on release!
|
||||
unrestrictedFeatures: [.interactiveLogin, .sharing],
|
||||
productsAtBuild: Configuration.IAPManager.productsAtBuild
|
||||
)
|
||||
let remoteRepository = AppData.cdProfileRepositoryV3(
|
||||
registry: .shared,
|
||||
coder: CodableProfileCoder(),
|
||||
context: remoteStore.context,
|
||||
observingResults: true
|
||||
) { error in
|
||||
pp_log(.app, .error, "Unable to decode remote result: \(error)")
|
||||
return .ignore
|
||||
}
|
||||
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()
|
||||
|
||||
return ProfileManager(
|
||||
repository: mainProfileRepository,
|
||||
backupRepository: backupProfileRepository,
|
||||
remoteRepository: remoteRepository,
|
||||
deletingRemotely: deletingRemotely,
|
||||
isIncluded: isProfileIncluded
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
// validate provider modules
|
||||
let profile = try builder.tryBuild()
|
||||
do {
|
||||
_ = try profile.withProviderModules()
|
||||
return profile
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
)
|
||||
let profileManager: ProfileManager = {
|
||||
let remoteStore = CoreDataPersistentStore(
|
||||
logger: .default,
|
||||
containerName: Constants.shared.containers.remote,
|
||||
model: AppData.cdProfilesModel,
|
||||
cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitId),
|
||||
author: nil
|
||||
)
|
||||
let remoteRepository = AppData.cdProfileRepositoryV3(
|
||||
registry: .shared,
|
||||
coder: CodableProfileCoder(),
|
||||
context: remoteStore.context,
|
||||
observingResults: true
|
||||
) { error in
|
||||
pp_log(.app, .error, "Unable to decode remote result: \(error)")
|
||||
return .ignore
|
||||
}
|
||||
return ProfileManager(
|
||||
repository: Configuration.ProfileManager.mainProfileRepository,
|
||||
backupRepository: Configuration.ProfileManager.backupProfileRepository,
|
||||
remoteRepository: remoteRepository,
|
||||
deletingRemotely: Configuration.ProfileManager.deletingRemotely,
|
||||
processor: processor
|
||||
)
|
||||
}()
|
||||
let tunnel = ExtendedTunnel(
|
||||
tunnel: Tunnel(strategy: Configuration.ExtendedTunnel.strategy),
|
||||
environment: tunnelEnvironment,
|
||||
processor: processor,
|
||||
interval: Constants.shared.tunnel.refreshInterval
|
||||
)
|
||||
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
|
||||
)
|
||||
}()
|
||||
}
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
private enum Configuration {
|
||||
}
|
||||
|
||||
extension Configuration {
|
||||
enum IAPManager {
|
||||
static var customUserLevel: AppUserLevel? {
|
||||
if let envString = ProcessInfo.processInfo.environment["CUSTOM_USER_LEVEL"],
|
||||
let envValue = Int(envString),
|
||||
let testAppType = AppUserLevel(rawValue: envValue) {
|
||||
|
||||
return testAppType
|
||||
}
|
||||
if let infoValue = BundleConfiguration.mainIntegerIfPresent(for: .customUserLevel),
|
||||
let testAppType = AppUserLevel(rawValue: infoValue) {
|
||||
|
||||
return testAppType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static let productsAtBuild: BuildProducts<AppProduct> = {
|
||||
#if os(iOS)
|
||||
if $0 <= 2016 {
|
||||
return [.Full.iOS]
|
||||
} else if $0 <= 3000 {
|
||||
return [.Features.networkSettings]
|
||||
}
|
||||
return []
|
||||
#elseif os(macOS)
|
||||
if $0 <= 3000 {
|
||||
return [.Features.networkSettings]
|
||||
}
|
||||
return []
|
||||
#else
|
||||
return []
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Configuration {
|
||||
enum ProfileManager {
|
||||
static let sharedTitle: @Sendable (Profile) -> String = {
|
||||
String(format: Constants.shared.tunnel.profileTitleFormat, $0.name)
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
private static let deletingRemotely = true
|
||||
static let deletingRemotely = true
|
||||
|
||||
private static let isProfileIncluded: (Profile) -> Bool = {
|
||||
$0.attributes.isAvailableForTV == true
|
||||
}
|
||||
static let isProfileIncluded: @Sendable (Profile) -> Bool = {
|
||||
$0.attributes.isAvailableForTV == true
|
||||
}
|
||||
#else
|
||||
private static let deletingRemotely = false
|
||||
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
|
||||
)
|
||||
|
||||
private static var customUserLevel: AppUserLevel? {
|
||||
if let envString = ProcessInfo.processInfo.environment["CUSTOM_USER_LEVEL"],
|
||||
let envValue = Int(envString),
|
||||
let testAppType = AppUserLevel(rawValue: envValue) {
|
||||
|
||||
return testAppType
|
||||
static let isProfileIncluded: (Profile) -> Bool = { _ in
|
||||
true
|
||||
}
|
||||
if let infoValue = BundleConfiguration.mainIntegerIfPresent(for: .customUserLevel),
|
||||
let testAppType = AppUserLevel(rawValue: infoValue) {
|
||||
|
||||
return testAppType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static let productsAtBuild: BuildProducts<AppProduct> = {
|
||||
#if os(iOS)
|
||||
if $0 <= 2016 {
|
||||
return [.Full.iOS]
|
||||
} else if $0 <= 3000 {
|
||||
return [.Features.networkSettings]
|
||||
}
|
||||
return []
|
||||
#elseif os(macOS)
|
||||
if $0 <= 3000 {
|
||||
return [.Features.networkSettings]
|
||||
}
|
||||
return []
|
||||
#else
|
||||
return []
|
||||
#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()
|
||||
do {
|
||||
_ = try profile.withProviderModules()
|
||||
return profile
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
|
||||
extension Tunnel {
|
||||
static let shared = Tunnel(
|
||||
strategy: FakeTunnelStrategy(environment: .shared, dataCountInterval: 1000)
|
||||
)
|
||||
extension Configuration {
|
||||
enum ExtendedTunnel {
|
||||
static var strategy: TunnelObservableStrategy {
|
||||
FakeTunnelStrategy(environment: .shared, dataCountInterval: 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ProfileManager {
|
||||
@MainActor
|
||||
extension Configuration.ProfileManager {
|
||||
static var mainProfileRepository: ProfileRepository {
|
||||
coreDataProfileRepository
|
||||
}
|
||||
|
@ -185,13 +224,16 @@ private extension ProfileManager {
|
|||
|
||||
#else
|
||||
|
||||
extension Tunnel {
|
||||
static let shared = Tunnel(
|
||||
strategy: ProfileManager.neStrategy
|
||||
)
|
||||
extension Configuration {
|
||||
enum ExtendedTunnel {
|
||||
static var strategy: TunnelObservableStrategy {
|
||||
ProfileManager.neStrategy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ProfileManager {
|
||||
@MainActor
|
||||
extension Configuration.ProfileManager {
|
||||
static var mainProfileRepository: ProfileRepository {
|
||||
neProfileRepository
|
||||
}
|
||||
|
@ -203,10 +245,11 @@ private extension ProfileManager {
|
|||
|
||||
#endif
|
||||
|
||||
private extension ProfileManager {
|
||||
@MainActor
|
||||
extension Configuration.ProfileManager {
|
||||
static let neProfileRepository: ProfileRepository = {
|
||||
NEProfileRepository(repository: neStrategy) {
|
||||
ProfileManager.sharedTitle($0)
|
||||
sharedTitle($0)
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -240,31 +283,6 @@ private extension ProfileManager {
|
|||
|
||||
// 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 {
|
||||
static var `default`: CoreDataPersistentStoreLogger {
|
||||
DefaultCoreDataPersistentStoreLogger()
|
||||
|
|
Loading…
Reference in New Issue