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:
Davide 2024-11-04 23:34:22 +01:00 committed by GitHub
parent 0c66050726
commit f3d13d0cdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 403 additions and 324 deletions

View File

@ -85,7 +85,6 @@ extension PassepartoutApp {
MenuBarExtra {
AppMenu(
profileManager: context.profileManager,
profileProcessor: context.profileProcessor,
tunnel: context.tunnel
)
.withEnvironment(from: context, theme: theme)

View File

@ -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(

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import CommonLibrary
import CommonUtils
import SwiftUI

View File

@ -30,9 +30,6 @@ import SwiftUI
struct ProviderEntitySelector: View {
@EnvironmentObject
private var profileProcessor: ProfileProcessor
@ObservedObject
var profileManager: ProfileManager

View File

@ -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 {

View File

@ -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()
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import CommonLibrary
import PassepartoutKit
import SwiftUI

View File

@ -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
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -26,7 +26,7 @@
import Foundation
import PassepartoutKit
public enum AppError {
public enum AppError: Error {
case emptyProfileName
case malformedModule(any ModuleBuilder, error: Error)

View File

@ -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]? {

View File

@ -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

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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
}
}
}
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import CommonLibrary
import CommonUtils
import PassepartoutKit
import SwiftUI

View File

@ -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
}
}

View File

@ -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()

View File

@ -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()

View File

@ -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 {
//

View File

@ -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()