diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index 3dfd9423..04d9df4f 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 0E021D9C284E68580077EF5D /* CoreContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E021D9A284E68580077EF5D /* CoreContext.swift */; }; + 0E021D9D284E68580077EF5D /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E021D9B284E68580077EF5D /* AppContext.swift */; }; 0E0392772818732D00827C10 /* BuildProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0392762818732D00827C10 /* BuildProducts.swift */; }; 0E039279281890B100827C10 /* AddHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E039278281890B100827C10 /* AddHostView.swift */; }; 0E065F112813269500062CAF /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E065F102813269500062CAF /* WelcomeView.swift */; }; @@ -17,6 +19,8 @@ 0E0C0729236087A100155AAC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E0C072B236087A100155AAC /* InfoPlist.strings */; }; 0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E12BC8E27F62C8500B2F912 /* Validators.swift */; }; 0E293851285A70AC002A6E0E /* AppPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E293850285A70AC002A6E0E /* AppPreference.swift */; }; + 0E293857285A73BC002A6E0E /* AppContext+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E293856285A73BC002A6E0E /* AppContext+Shared.swift */; }; + 0E29385C285A8B30002A6E0E /* CoreContext+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E29385B285A8B30002A6E0E /* CoreContext+Shared.swift */; }; 0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */; }; 0E2A8D4F27B04BBA00207D04 /* OrganizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */; }; 0E2AC24522EC3AC10037B4B0 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 0E2AC24422EC3AC10037B4B0 /* Settings.bundle */; }; @@ -123,7 +127,6 @@ 0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */; }; 0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */; }; 0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */; }; - 0EF2CC03285AFED800E501D5 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2CC02285AFED800E501D5 /* AppContext.swift */; }; 0EF8C5A828213C510053CE89 /* OrganizerView+Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */; }; /* End PBXBuildFile section */ @@ -191,6 +194,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0E021D9A284E68580077EF5D /* CoreContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreContext.swift; sourceTree = ""; }; + 0E021D9B284E68580077EF5D /* AppContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; 0E0392762818732D00827C10 /* BuildProducts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildProducts.swift; sourceTree = ""; }; 0E039278281890B100827C10 /* AddHostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHostView.swift; sourceTree = ""; }; 0E065F102813269500062CAF /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; @@ -204,6 +209,8 @@ 0E1C0A52238FFF97009FC087 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; 0E23B4A12298559800304C30 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 0E293850285A70AC002A6E0E /* AppPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreference.swift; sourceTree = ""; }; + 0E293856285A73BC002A6E0E /* AppContext+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+Shared.swift"; sourceTree = ""; }; + 0E29385B285A8B30002A6E0E /* CoreContext+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreContext+Shared.swift"; sourceTree = ""; }; 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassepartoutApp.swift; sourceTree = ""; }; 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizerView.swift; sourceTree = ""; }; 0E2AC24422EC3AC10037B4B0 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -342,7 +349,6 @@ 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderView.swift; sourceTree = ""; }; 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProfileView.swift; sourceTree = ""; }; 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderViewModel.swift; sourceTree = ""; }; - 0EF2CC02285AFED800E501D5 /* AppContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Profiles.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -378,6 +384,32 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0E293858285A7484002A6E0E /* Constants */ = { + isa = PBXGroup; + children = ( + 0EB17EA127D2263700D473B5 /* Constants.swift */, + ); + path = Constants; + sourceTree = ""; + }; + 0E293859285A7489002A6E0E /* Contexts */ = { + isa = PBXGroup; + children = ( + 0E021D9A284E68580077EF5D /* CoreContext.swift */, + 0E29385B285A8B30002A6E0E /* CoreContext+Shared.swift */, + ); + path = Contexts; + sourceTree = ""; + }; + 0E29385A285A749E002A6E0E /* Contexts */ = { + isa = PBXGroup; + children = ( + 0E021D9B284E68580077EF5D /* AppContext.swift */, + 0E293856285A73BC002A6E0E /* AppContext+Shared.swift */, + ); + path = Contexts; + sourceTree = ""; + }; 0E2C171C27CB6307007E8488 /* Reusable */ = { isa = PBXGroup; children = ( @@ -533,6 +565,7 @@ isa = PBXGroup; children = ( 0EB17EA027D2263700D473B5 /* Constants */, + 0E29385A285A749E002A6E0E /* Contexts */, 0E49F6C927DB398100385834 /* Extensions */, 0E92781227E7CD530057BB81 /* InApp */, 0EA591112733DD4E0096F796 /* Intents */, @@ -567,7 +600,6 @@ 0EB17EA027D2263700D473B5 /* Constants */ = { isa = PBXGroup; children = ( - 0EF2CC02285AFED800E501D5 /* AppContext.swift */, 0E293850285A70AC002A6E0E /* AppPreference.swift */, 0EB17EA527D2263700D473B5 /* Constants+Extensions.swift */, 0E6059CE27FCC618003F4063 /* SwiftGen+Assets.swift */, @@ -597,7 +629,8 @@ 0ED30DD627EA33220057D8A3 /* Shared */ = { isa = PBXGroup; children = ( - 0EB17EA127D2263700D473B5 /* Constants.swift */, + 0E293858285A7484002A6E0E /* Constants */, + 0E293859285A7489002A6E0E /* Contexts */, ); path = Shared; sourceTree = ""; @@ -880,6 +913,7 @@ 0EA591162733DDDA0096F796 /* Intents.intentdefinition in Sources */, 0E34AC7827F840890042F2AB /* OrganizerView+Scene.swift in Sources */, 0E0BD27927B2EBE500583AC5 /* ShortcutsView.swift in Sources */, + 0E29385C285A8B30002A6E0E /* CoreContext+Shared.swift in Sources */, 0E92D7C627F103300033CB7B /* ProfileView+Configuration.swift in Sources */, 0E2DE71C27DCCFE80067B9E1 /* TunnelKit+Identifiable.swift in Sources */, 0ED1D6DE27DBA42100983466 /* DiagnosticsView+WireGuard.swift in Sources */, @@ -935,8 +969,8 @@ 0E293851285A70AC002A6E0E /* AppPreference.swift in Sources */, 0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */, 0E2C172B27CB63F9007E8488 /* Reviewer.swift in Sources */, - 0EF2CC03285AFED800E501D5 /* AppContext.swift in Sources */, 0E71ACDD27C0295C00F85C4B /* View+Extensions.swift in Sources */, + 0E021D9C284E68580077EF5D /* CoreContext.swift in Sources */, 0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */, 0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */, 0E6059CF27FCC618003F4063 /* SwiftGen+Assets.swift in Sources */, @@ -955,6 +989,7 @@ 0E3CD483280DAE92007075C0 /* ProfileView+MainMenu.swift in Sources */, 0EB17EAE27D226CF00D473B5 /* LocalProduct.swift in Sources */, 0E71ACEB27C1060D00F85C4B /* EndpointView.swift in Sources */, + 0E293857285A73BC002A6E0E /* AppContext+Shared.swift in Sources */, 0E53249927D26B51002565C3 /* ProductManager.swift in Sources */, 0E9C233027F47032007D5FC7 /* IntentsManager.swift in Sources */, 0EB4042C27CA0E8C00378B1A /* Unlocalized.swift in Sources */, @@ -968,6 +1003,7 @@ 0E2C171B27CB5A3B007E8488 /* GenericCreditsView.swift in Sources */, 0ED30DD227EA1F650057D8A3 /* PaywallView+Purchase.swift in Sources */, 0EB3413027C7761A00483410 /* Binding+Extensions.swift in Sources */, + 0E021D9D284E68580077EF5D /* AppContext.swift in Sources */, 0E2DE72527DCDF550067B9E1 /* WireGuard+L10n.swift in Sources */, 0E71ACFB27C12E5300F85C4B /* VersionView.swift in Sources */, 0ED1D6DC27DBA41700983466 /* DiagnosticsView+OpenVPN.swift in Sources */, diff --git a/Passepartout/App/Constants/Constants+Extensions.swift b/Passepartout/App/Constants/Constants+Extensions.swift index 9762c0b6..e198bdc4 100644 --- a/Passepartout/App/Constants/Constants+Extensions.swift +++ b/Passepartout/App/Constants/Constants+Extensions.swift @@ -156,7 +156,7 @@ extension Constants { return .init(rawValue: levelNum) ?? .info }() - static let appLogFormat = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M" + static let logFormat = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M" private static let appFileName = "Debug.log" diff --git a/Passepartout/App/Contexts/AppContext+Shared.swift b/Passepartout/App/Contexts/AppContext+Shared.swift new file mode 100644 index 00000000..645de501 --- /dev/null +++ b/Passepartout/App/Contexts/AppContext+Shared.swift @@ -0,0 +1,42 @@ +// +// AppContext+Shared.swift +// Passepartout +// +// Created by Davide De Rosa on 6/15/22. +// Copyright (c) 2022 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 . +// + +import Foundation + +extension AppContext { + static let shared = AppContext(coreContext: .shared) +} + +extension ProductManager { + static let shared = AppContext.shared.productManager +} + +extension IntentsManager { + static let shared = AppContext.shared.intentsManager +} + +extension Reviewer { + static let shared = AppContext.shared.reviewer +} diff --git a/Passepartout/App/Contexts/AppContext.swift b/Passepartout/App/Contexts/AppContext.swift new file mode 100644 index 00000000..97eb1589 --- /dev/null +++ b/Passepartout/App/Contexts/AppContext.swift @@ -0,0 +1,94 @@ +// +// AppContext.swift +// Passepartout +// +// Created by Davide De Rosa on 3/17/22. +// Copyright (c) 2022 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 . +// + +import Foundation +import Combine +import PassepartoutCore + +@MainActor +class AppContext { + private let logManager: LogManager + + let productManager: ProductManager + + let intentsManager: IntentsManager + + let reviewer: Reviewer + + private var cancellables: Set = [] + + init(coreContext: CoreContext) { + logManager = LogManager(logFile: Constants.Log.appFileURL) + logManager.logLevel = Constants.Log.logLevel + logManager.logFormat = Constants.Log.logFormat + logManager.configureLogging() + pp_log.info("Logging to: \(logManager.logFile!)") + + productManager = ProductManager( + appType: Constants.InApp.appType, + buildProducts: Constants.InApp.buildProducts + ) + intentsManager = IntentsManager() + reviewer = Reviewer() + reviewer.eventCountBeforeRating = Constants.Rating.eventCount + + // post + + configureObjects(coreContext: coreContext) + } + + private func configureObjects(coreContext: CoreContext) { + coreContext.vpnManager.currentState.$vpnStatus + .removeDuplicates() + .sink { + if $0 == .connected { + pp_log.info("VPN successful connection, report to Reviewer") + self.reviewer.reportEvent() + } + }.store(in: &cancellables) + + coreContext.vpnManager.isOnDemandRulesSupported = { + self.isEligibleForOnDemandRules() + } + } + + // eligibility: ignore network settings if ineligible + private func isEligibleForNetworkSettings() -> Bool { + guard productManager.isEligible(forFeature: .networkSettings) else { + pp_log.warning("Ignore network settings, not eligible") + return false + } + return true + } + + // eligibility: reset on-demand rules if no trusted networks + private func isEligibleForOnDemandRules() -> Bool { + guard productManager.isEligible(forFeature: .trustedNetworks) else { + pp_log.warning("Ignore on-demand rules, not eligible for trusted networks") + return false + } + return true + } +} diff --git a/Passepartout/App/PassepartoutApp.swift b/Passepartout/App/PassepartoutApp.swift index 58aca2b8..b1cbf54e 100644 --- a/Passepartout/App/PassepartoutApp.swift +++ b/Passepartout/App/PassepartoutApp.swift @@ -56,7 +56,7 @@ extension View { return } pp_log.info("Handling activity: \(activity.name)") - activity.handler(userActivity, AppContext.shared.vpnManager) + activity.handler(userActivity, CoreContext.shared.vpnManager) } } } diff --git a/Passepartout/App/Views/InfoMenu.swift b/Passepartout/App/Views/InfoMenu.swift index 124f6fdb..4cccb5e8 100644 --- a/Passepartout/App/Views/InfoMenu.swift +++ b/Passepartout/App/Views/InfoMenu.swift @@ -174,7 +174,7 @@ struct InfoMenu: View { extension InfoMenu { private var testSection: some View { Button("Export providers") { - guard let urls = AppContext.shared.urlsForProviders else { + guard let urls = CoreContext.shared.urlsForProviders else { return } modalType = .exportProviders(urls) diff --git a/Passepartout/Shared/Constants.swift b/Passepartout/Shared/Constants/Constants.swift similarity index 100% rename from Passepartout/Shared/Constants.swift rename to Passepartout/Shared/Constants/Constants.swift diff --git a/Passepartout/Shared/Contexts/CoreContext+Shared.swift b/Passepartout/Shared/Contexts/CoreContext+Shared.swift new file mode 100644 index 00000000..fcefc34c --- /dev/null +++ b/Passepartout/Shared/Contexts/CoreContext+Shared.swift @@ -0,0 +1,53 @@ +// +// CoreContext.swift +// Passepartout +// +// Created by Davide De Rosa on 6/15/22. +// Copyright (c) 2022 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 . +// + +import Foundation +import PassepartoutCore + +extension CoreContext { + static let shared = CoreContext(store: UserDefaultsStore(defaults: .standard)) +} + +extension UpgradeManager { + static let shared = CoreContext.shared.upgradeManager +} + +extension ProfileManager { + static let shared = CoreContext.shared.profileManager +} + +extension ProviderManager { + static let shared = CoreContext.shared.providerManager +} + +extension VPNManager { + static let shared = CoreContext.shared.vpnManager +} + +extension VPNManager.ObservableState { + + @MainActor + static let shared = CoreContext.shared.vpnManager.currentState +} diff --git a/Passepartout/App/Constants/AppContext.swift b/Passepartout/Shared/Contexts/CoreContext.swift similarity index 55% rename from Passepartout/App/Constants/AppContext.swift rename to Passepartout/Shared/Contexts/CoreContext.swift index b7692a48..19607ee7 100644 --- a/Passepartout/App/Constants/AppContext.swift +++ b/Passepartout/Shared/Contexts/CoreContext.swift @@ -1,8 +1,8 @@ // -// AppContext.swift +// CoreContext.swift // Passepartout // -// Created by Davide De Rosa on 3/17/22. +// Created by Davide De Rosa on 6/4/22. // Copyright (c) 2022 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -24,16 +24,13 @@ // import Foundation -import CoreData import Combine import PassepartoutCore import PassepartoutServices @MainActor -class AppContext { - static let shared = AppContext() - - private let logManager: LogManager +class CoreContext { + let store: KeyValueStore private let profilesPersistence: Persistence @@ -55,22 +52,10 @@ class AppContext { let vpnManager: VPNManager - let productManager: ProductManager - - let intentsManager: IntentsManager - - let reviewer: Reviewer - private var cancellables: Set = [] - private init() { - let store = UserDefaultsStore(defaults: .standard) - - logManager = LogManager(logFile: Constants.Log.appFileURL) - logManager.logLevel = Constants.Log.logLevel - logManager.logFormat = Constants.Log.appLogFormat - logManager.configureLogging() - pp_log.info("Logging to: \(logManager.logFile!)") + init(store: KeyValueStore) { + self.store = store let persistenceManager = PersistenceManager(store: store) profilesPersistence = persistenceManager.profilesPersistence( @@ -119,15 +104,6 @@ class AppContext { providerManager: providerManager, strategy: strategy ) - - // app - - productManager = ProductManager( - appType: Constants.InApp.appType, - buildProducts: Constants.InApp.buildProducts - ) - intentsManager = IntentsManager() - reviewer = Reviewer() // post @@ -135,80 +111,10 @@ class AppContext { } private func configureObjects() { - - // core - providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager vpnManager.tunnelLogFormat = Constants.Log.tunnelLogFormat - vpnManager.isOnDemandRulesSupported = { - self.isEligibleForOnDemandRules() - } profileManager.observeUpdates() vpnManager.observeUpdates() - - // app - - reviewer.eventCountBeforeRating = Constants.Rating.eventCount - vpnManager.currentState.$vpnStatus - .removeDuplicates() - .sink { - if $0 == .connected { - pp_log.info("VPN successful connection, report to Reviewer") - self.reviewer.reportEvent() - } - }.store(in: &cancellables) - } - - // eligibility: ignore network settings if ineligible - private func isEligibleForNetworkSettings() -> Bool { - guard productManager.isEligible(forFeature: .networkSettings) else { - pp_log.warning("Ignore network settings, not eligible") - return false - } - return true - } - - // eligibility: reset on-demand rules if no trusted networks - private func isEligibleForOnDemandRules() -> Bool { - guard productManager.isEligible(forFeature: .trustedNetworks) else { - pp_log.warning("Ignore on-demand rules, not eligible for trusted networks") - return false - } - return true } } - -extension UpgradeManager { - static let shared = AppContext.shared.upgradeManager -} - -extension ProfileManager { - static let shared = AppContext.shared.profileManager -} - -extension ProviderManager { - static let shared = AppContext.shared.providerManager -} - -extension VPNManager { - static let shared = AppContext.shared.vpnManager -} - -extension ProductManager { - static let shared = AppContext.shared.productManager -} - -extension IntentsManager { - static let shared = AppContext.shared.intentsManager -} - -extension Reviewer { - static let shared = AppContext.shared.reviewer -} - -extension VPNManager.ObservableState { - - @MainActor - static let shared = AppContext.shared.vpnManager.currentState -}