diff --git a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f748ed03..e058c40f 100644 --- a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,7 +41,7 @@ "kind" : "remoteSourceControl", "location" : "git@github.com:passepartoutvpn/passepartoutkit-source", "state" : { - "revision" : "d74cd0f02ba844beff2be55bf5f93796a3c43a6d" + "revision" : "39cd828d3ee7cb502c4c0e36e3dc42e45bfae10b" } }, { diff --git a/Passepartout/Library/Package.swift b/Passepartout/Library/Package.swift index a1e8d052..1cb6c7da 100644 --- a/Passepartout/Library/Package.swift +++ b/Passepartout/Library/Package.swift @@ -44,7 +44,7 @@ let package = Package( ], dependencies: [ // .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.11.0"), - .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "d74cd0f02ba844beff2be55bf5f93796a3c43a6d"), + .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "39cd828d3ee7cb502c4c0e36e3dc42e45bfae10b"), // .package(path: "../../../passepartoutkit-source"), .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.9.1"), // .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"), diff --git a/Passepartout/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift b/Passepartout/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift index ca84bc07..ec3771ad 100644 --- a/Passepartout/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift +++ b/Passepartout/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift @@ -31,28 +31,22 @@ import Foundation import PassepartoutKit extension AppData { - public static func cdProviderRepositoryV3( - context: NSManagedObjectContext, - backgroundContext: NSManagedObjectContext - ) -> ProviderRepository { - CDProviderRepositoryV3(context: context, backgroundContext: backgroundContext) + public static func cdProviderRepositoryV3(context: NSManagedObjectContext) -> ProviderRepository { + CDProviderRepositoryV3(context: context) } } actor CDProviderRepositoryV3: NSObject, ProviderRepository { private nonisolated let context: NSManagedObjectContext - private nonisolated let backgroundContext: NSManagedObjectContext - private nonisolated let providersSubject: CurrentValueSubject<[ProviderMetadata], Never> private nonisolated let lastUpdateSubject: CurrentValueSubject<[ProviderID: Date], Never> private nonisolated let providersController: NSFetchedResultsController - init(context: NSManagedObjectContext, backgroundContext: NSManagedObjectContext) { + init(context: NSManagedObjectContext) { self.context = context - self.backgroundContext = backgroundContext providersSubject = CurrentValueSubject([]) lastUpdateSubject = CurrentValueSubject([:]) @@ -90,7 +84,7 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository { } func store(_ index: [ProviderMetadata]) async throws { - try await backgroundContext.perform { [weak self] in + try await context.perform { [weak self] in guard let self else { return } @@ -101,25 +95,25 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository { let lastUpdatesByProvider = results.reduce(into: [:]) { $0[$1.providerId] = $1.lastUpdate } - results.forEach(backgroundContext.delete) + results.forEach(context.delete) // replace but retain last update - let mapper = CoreDataMapper(context: backgroundContext) + let mapper = CoreDataMapper(context: context) index.forEach { let lastUpdate = lastUpdatesByProvider[$0.id.rawValue] mapper.cdProvider(from: $0, lastUpdate: lastUpdate) } - try backgroundContext.save() + try context.save() } catch { - backgroundContext.rollback() + context.rollback() throw error } } } func store(_ infrastructure: VPNInfrastructure, for providerId: ProviderID) async throws { - try await backgroundContext.perform { [weak self] in + try await context.perform { [weak self] in guard let self else { return } @@ -138,15 +132,15 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository { let serverRequest = CDVPNServerV3.fetchRequest() serverRequest.predicate = predicate let servers = try serverRequest.execute() - servers.forEach(backgroundContext.delete) + servers.forEach(context.delete) let presetRequest = CDVPNPresetV3.fetchRequest() presetRequest.predicate = predicate let presets = try presetRequest.execute() - presets.forEach(backgroundContext.delete) + presets.forEach(context.delete) // create new entities - let mapper = CoreDataMapper(context: backgroundContext) + let mapper = CoreDataMapper(context: context) try infrastructure.servers.forEach { try mapper.cdServer(from: $0) } @@ -154,9 +148,9 @@ actor CDProviderRepositoryV3: NSObject, ProviderRepository { try mapper.cdPreset(from: $0) } - try backgroundContext.save() + try context.save() } catch { - backgroundContext.rollback() + context.rollback() throw error } } diff --git a/Passepartout/Library/Sources/CommonLibrary/Business/ProfileManager.swift b/Passepartout/Library/Sources/CommonLibrary/Business/ProfileManager.swift index 2d2fee50..8ea6f55a 100644 --- a/Passepartout/Library/Sources/CommonLibrary/Business/ProfileManager.swift +++ b/Passepartout/Library/Sources/CommonLibrary/Business/ProfileManager.swift @@ -57,9 +57,6 @@ public final class ProfileManager: ObservableObject { // MARK: State - @Published - private var profiles: [Profile] - private var allProfiles: [Profile.ID: Profile] { didSet { reloadFilteredProfiles(with: searchSubject.value) @@ -68,14 +65,11 @@ public final class ProfileManager: ObservableObject { private var allRemoteProfiles: [Profile.ID: Profile] + private var filteredProfiles: [Profile] + @Published public private(set) var isRemoteImportingEnabled: Bool - public var isReady: Bool { - waitingObservers.isEmpty - } - - @Published private var waitingObservers: Set // MARK: Publishers @@ -84,9 +78,13 @@ public final class ProfileManager: ObservableObject { private let searchSubject: CurrentValueSubject - private var subscriptions: Set + private var localSubscription: AnyCancellable? - private var remoteSubscriptions: Set + private var remoteSubscription: AnyCancellable? + + private var searchSubscription: AnyCancellable? + + private var remoteImportTask: Task? // for testing/previews public init(profiles: [Profile]) { @@ -98,18 +96,18 @@ public final class ProfileManager: ObservableObject { mirrorsRemoteRepository = false processor = nil - self.profiles = [] allProfiles = profiles.reduce(into: [:]) { $0[$1.id] = $1 } allRemoteProfiles = [:] + filteredProfiles = [] isRemoteImportingEnabled = false waitingObservers = [] didChange = PassthroughSubject() searchSubject = CurrentValueSubject("") - subscriptions = [] - remoteSubscriptions = [] + + observeSearch() } public init( @@ -126,9 +124,9 @@ public final class ProfileManager: ObservableObject { self.mirrorsRemoteRepository = mirrorsRemoteRepository self.processor = processor - profiles = [] allProfiles = [:] allRemoteProfiles = [:] + filteredProfiles = [] isRemoteImportingEnabled = false if remoteRepositoryBlock != nil { waitingObservers = [.local, .remote] @@ -138,16 +136,20 @@ public final class ProfileManager: ObservableObject { didChange = PassthroughSubject() searchSubject = CurrentValueSubject("") - subscriptions = [] - remoteSubscriptions = [] + + observeSearch() } } -// MARK: - CRUD +// MARK: - View extension ProfileManager { + public var isReady: Bool { + waitingObservers.isEmpty + } + public var hasProfiles: Bool { - !profiles.isEmpty + !filteredProfiles.isEmpty } public var isSearching: Bool { @@ -155,21 +157,25 @@ extension ProfileManager { } public var headers: [ProfileHeader] { - profiles.map { + filteredProfiles.map { $0.header() } } + public func profile(withId profileId: Profile.ID) -> Profile? { + filteredProfiles.first { + $0.id == profileId + } + } + public func search(byName name: String) { searchSubject.send(name) } +} - public func profile(withId profileId: Profile.ID) -> Profile? { - profiles.first { - $0.id == profileId - } - } +// MARK: - CRUD +extension ProfileManager { public func save(_ originalProfile: Profile, force: Bool = false, remotelyShared: Bool? = nil) async throws { let profile: Profile if force { @@ -194,7 +200,6 @@ extension ProfileManager { try await backupRepository.saveProfile(profile) } } - allProfiles[profile.id] = profile didChange.send(.save(profile)) } else { pp_log(.App.profiles, .notice, "\tProfile \(profile.id) not modified, not saving") @@ -203,8 +208,8 @@ extension ProfileManager { pp_log(.App.profiles, .fault, "\tUnable to save profile \(profile.id): \(error)") throw error } - do { - if let remotelyShared, let remoteRepository { + if let remotelyShared, let remoteRepository { + do { if remotelyShared { pp_log(.App.profiles, .notice, "\tEnable remote sharing of profile \(profile.id)...") try await remoteRepository.saveProfile(profile) @@ -212,10 +217,10 @@ extension ProfileManager { pp_log(.App.profiles, .notice, "\tDisable remote sharing of profile \(profile.id)...") try await remoteRepository.removeProfiles(withIds: [profile.id]) } + } catch { + pp_log(.App.profiles, .fault, "\tUnable to save/remove remote profile \(profile.id): \(error)") + throw error } - } catch { - pp_log(.App.profiles, .fault, "\tUnable to save/remove remote profile \(profile.id): \(error)") - throw error } pp_log(.App.profiles, .notice, "Finished saving profile \(profile.id)") } @@ -227,21 +232,8 @@ extension ProfileManager { public func remove(withIds profileIds: [Profile.ID]) async { pp_log(.App.profiles, .notice, "Remove profiles \(profileIds)...") do { - // remove local profiles - var newAllProfiles = allProfiles try await repository.removeProfiles(withIds: profileIds) - profileIds.forEach { - newAllProfiles.removeValue(forKey: $0) - } - - // remove remote counterpart too try? await remoteRepository?.removeProfiles(withIds: profileIds) - profileIds.forEach { - allRemoteProfiles.removeValue(forKey: $0) - } - - // publish update - allProfiles = newAllProfiles didChange.send(.remove(profileIds)) } catch { pp_log(.App.profiles, .fault, "Unable to remove profiles \(profileIds): \(error)") @@ -299,7 +291,7 @@ extension ProfileManager { private extension ProfileManager { func firstUniqueName(from name: String) -> String { - let allNames = profiles.map(\.name) + let allNames = Set(allProfiles.values.map(\.name)) var newName = name var index = 1 while true { @@ -315,27 +307,18 @@ private extension ProfileManager { // MARK: - Observation extension ProfileManager { - public func observeLocal(searchDebounce: Int = 200) async throws { - subscriptions.removeAll() - + public func observeLocal() async throws { + localSubscription = nil let initialProfiles = try await repository.fetchProfiles() reloadLocalProfiles(initialProfiles) - repository + localSubscription = repository .profilesPublisher .dropFirst() .receive(on: DispatchQueue.main) .sink { [weak self] in self?.reloadLocalProfiles($0) } - .store(in: &subscriptions) - - searchSubject - .debounce(for: .milliseconds(searchDebounce), scheduler: DispatchQueue.main) - .sink { [weak self] in - self?.performSearch($0) - } - .store(in: &subscriptions) } public func observeRemote(_ isRemoteImportingEnabled: Bool) async throws { @@ -348,21 +331,30 @@ extension ProfileManager { } self.isRemoteImportingEnabled = isRemoteImportingEnabled - remoteSubscriptions.removeAll() + remoteSubscription = nil let newRepository = remoteRepositoryBlock(isRemoteImportingEnabled) let initialProfiles = try await newRepository.fetchProfiles() reloadRemoteProfiles(initialProfiles) remoteRepository = newRepository - remoteRepository? + remoteSubscription = remoteRepository? .profilesPublisher .dropFirst() .receive(on: DispatchQueue.main) .sink { [weak self] in self?.reloadRemoteProfiles($0) } - .store(in: &remoteSubscriptions) + } +} + +private extension ProfileManager { + func observeSearch(debounce: Int = 200) { + searchSubscription = searchSubject + .debounce(for: .milliseconds(debounce), scheduler: DispatchQueue.main) + .sink { [weak self] in + self?.reloadFilteredProfiles(with: $0) + } } } @@ -373,24 +365,11 @@ private extension ProfileManager { $0[$1.id] = $1 } if waitingObservers.contains(.local) { - waitingObservers.remove(.local) + waitingObservers.remove(.local) // @Published } + deleteExcludedProfiles() - // should not be imported at all, but you never know - if let processor { - let idsToRemove: [Profile.ID] = allProfiles - .filter { - !processor.isIncluded($0.value) - } - .map(\.key) - - if !idsToRemove.isEmpty { - pp_log(.App.profiles, .info, "Delete non-included local profiles: \(idsToRemove)") - Task.detached { - try await self.repository.removeProfiles(withIds: idsToRemove) - } - } - } + objectWillChange.send() } func reloadRemoteProfiles(_ result: [Profile]) { @@ -399,41 +378,74 @@ private extension ProfileManager { $0[$1.id] = $1 } if waitingObservers.contains(.remote) { - waitingObservers.remove(.remote) + waitingObservers.remove(.remote) // @Published + } + importRemoteProfiles(result) + + objectWillChange.send() + } + + // should not be imported at all, but you never know + func deleteExcludedProfiles() { + guard let processor else { + return + } + let idsToRemove: [Profile.ID] = allProfiles + .filter { + !processor.isIncluded($0.value) + } + .map(\.key) + + if !idsToRemove.isEmpty { + pp_log(.App.profiles, .info, "Delete non-included local profiles: \(idsToRemove)") + Task.detached { + try await self.repository.removeProfiles(withIds: idsToRemove) + } + } + } + + func importRemoteProfiles(_ profiles: [Profile]) { + guard !profiles.isEmpty else { + return } - Task.detached { [weak self] in + pp_log(.App.profiles, .info, "Start importing remote profiles: \(profiles.map(\.id)))") + assert(profiles.count == Set(profiles.map(\.id)).count, "Remote repository must not have duplicates") + + pp_log(.App.profiles, .debug, "Local attributes:") + let localAttributes: [Profile.ID: ProfileAttributes] = allProfiles.values.reduce(into: [:]) { + $0[$1.id] = $1.attributes + pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)") + } + pp_log(.App.profiles, .debug, "Remote attributes:") + let remoteAttributes: [Profile.ID: ProfileAttributes] = profiles.reduce(into: [:]) { + $0[$1.id] = $1.attributes + pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)") + } + + let remotelyDeletedIds = Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys)) + let mirrorsRemoteRepository = mirrorsRemoteRepository + + let previousTask = remoteImportTask + remoteImportTask = Task.detached { [weak self] in guard let self else { return } - pp_log(.App.profiles, .info, "Start importing remote profiles...") - assert(result.count == Set(result.map(\.id)).count, "Remote repository must not have duplicates") - - pp_log(.App.profiles, .debug, "Local attributes:") - let localAttributes: [Profile.ID: ProfileAttributes] = await allProfiles.values.reduce(into: [:]) { - $0[$1.id] = $1.attributes - pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)") + if let previousTask { + pp_log(.App.profiles, .info, "Cancel ongoing remote import...") + previousTask.cancel() + await previousTask.value } - pp_log(.App.profiles, .debug, "Remote attributes:") - let remoteAttributes: [Profile.ID: ProfileAttributes] = result.reduce(into: [:]) { - $0[$1.id] = $1.attributes - pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)") - } - - let profilesToImport = result - let remotelyDeletedIds = await Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys)) - let mirrorsRemoteRepository = mirrorsRemoteRepository var idsToRemove: [Profile.ID] = [] if !remotelyDeletedIds.isEmpty { pp_log(.App.profiles, .info, "Will \(mirrorsRemoteRepository ? "delete" : "retain") local profiles not present in remote repository: \(remotelyDeletedIds)") - if mirrorsRemoteRepository { idsToRemove.append(contentsOf: remotelyDeletedIds) } } - for remoteProfile in profilesToImport { + for remoteProfile in profiles { do { guard processor?.isIncluded(remoteProfile) ?? true else { pp_log(.App.profiles, .info, "Will delete non-included remote profile \(remoteProfile.id)") @@ -452,19 +464,26 @@ private extension ProfileManager { } catch { pp_log(.App.profiles, .error, "Unable to import remote profile: \(error)") } + guard !Task.isCancelled else { + pp_log(.App.profiles, .info, "Cancelled import of remote profiles: \(profiles.map(\.id))") + return + } } + pp_log(.App.profiles, .notice, "Finished importing remote profiles, delete stale profiles: \(idsToRemove)") - try? await repository.removeProfiles(withIds: idsToRemove) + do { + try await repository.removeProfiles(withIds: idsToRemove) + } catch { + pp_log(.App.profiles, .error, "Unable to delete stale profiles: \(error)") + } + + // yield a little bit + try? await Task.sleep(for: .milliseconds(100)) } } - func performSearch(_ search: String) { - pp_log(.App.profiles, .notice, "Filter profiles with '\(search)'") - reloadFilteredProfiles(with: search) - } - func reloadFilteredProfiles(with search: String) { - profiles = allProfiles + filteredProfiles = allProfiles .values .filter { if !search.isEmpty { @@ -475,5 +494,9 @@ private extension ProfileManager { .sorted { $0.name.lowercased() < $1.name.lowercased() } + + pp_log(.App.profiles, .notice, "Filter profiles with '\(search)' (\(filteredProfiles.count) results)") + + objectWillChange.send() } } diff --git a/Passepartout/Library/Sources/CommonLibrary/IAP/IAPManager.swift b/Passepartout/Library/Sources/CommonLibrary/IAP/IAPManager.swift index 0ac7f69a..df4c7953 100644 --- a/Passepartout/Library/Sources/CommonLibrary/IAP/IAPManager.swift +++ b/Passepartout/Library/Sources/CommonLibrary/IAP/IAPManager.swift @@ -261,7 +261,6 @@ extension IAPManager { } } .store(in: &subscriptions) - } catch { pp_log(.App.iap, .error, "Unable to fetch in-app products: \(error)") } diff --git a/Passepartout/Library/Sources/CommonUtils/Business/CoreDataRepository.swift b/Passepartout/Library/Sources/CommonUtils/Business/CoreDataRepository.swift index 2e4dfb4f..75902170 100644 --- a/Passepartout/Library/Sources/CommonUtils/Business/CoreDataRepository.swift +++ b/Passepartout/Library/Sources/CommonUtils/Business/CoreDataRepository.swift @@ -50,6 +50,8 @@ public actor CoreDataRepository: NSObject, private let observingResults: Bool + private let beforeFetch: ((NSFetchRequest) -> Void)? + private nonisolated let fromMapper: (CD) throws -> T? private nonisolated let toMapper: (T, NSManagedObjectContext) throws -> CD @@ -58,8 +60,7 @@ public actor CoreDataRepository: NSObject, private nonisolated let entitiesSubject: CurrentValueSubject, Never> - // cannot easily use CD as generic - private var resultsController: NSFetchedResultsController + private var resultsController: NSFetchedResultsController? public init( context: NSManagedObjectContext, @@ -76,19 +77,11 @@ public actor CoreDataRepository: NSObject, self.entityName = entityName self.context = context self.observingResults = observingResults + self.beforeFetch = beforeFetch self.fromMapper = fromMapper self.toMapper = toMapper self.onResultError = onResultError entitiesSubject = CurrentValueSubject(EntitiesResult()) - - let request = NSFetchRequest(entityName: entityName) - beforeFetch?(request) - resultsController = NSFetchedResultsController( - fetchRequest: request, - managedObjectContext: context, - sectionNameKeyPath: nil, - cacheName: nil - ) } public nonisolated var entitiesPublisher: AnyPublisher, Never> { @@ -183,17 +176,24 @@ private extension CoreDataRepository { @discardableResult func filter(byPredicate predicate: NSPredicate?) async throws -> [T] { - let request = resultsController.fetchRequest + let request = newFetchRequest() request.predicate = predicate - resultsController = NSFetchedResultsController( - fetchRequest: request, - managedObjectContext: context, - sectionNameKeyPath: nil, - cacheName: nil - ) - resultsController.delegate = self - try resultsController.performFetch() - return await sendResults(from: resultsController) + beforeFetch?(request) + + let newController = try await context.perform { + let newController = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: self.context, + sectionNameKeyPath: nil, + cacheName: nil + ) + newController.delegate = self + try newController.performFetch() + return newController + } + + resultsController = newController + return await sendResults(from: newController) } @discardableResult diff --git a/Passepartout/Library/Sources/UILibrary/Business/AppContext.swift b/Passepartout/Library/Sources/UILibrary/Business/AppContext.swift index 5ad0f5bc..6b2d8b49 100644 --- a/Passepartout/Library/Sources/UILibrary/Business/AppContext.swift +++ b/Passepartout/Library/Sources/UILibrary/Business/AppContext.swift @@ -43,6 +43,8 @@ public final class AppContext: ObservableObject { public let tunnel: ExtendedTunnel + private let tunnelReceiptURL: URL + private var launchTask: Task? private var pendingTask: Task? @@ -55,7 +57,8 @@ public final class AppContext: ObservableObject { profileManager: ProfileManager, providerManager: ProviderManager, registry: Registry, - tunnel: ExtendedTunnel + tunnel: ExtendedTunnel, + tunnelReceiptURL: URL ) { self.iapManager = iapManager self.migrationManager = migrationManager @@ -63,6 +66,7 @@ public final class AppContext: ObservableObject { self.providerManager = providerManager self.registry = registry self.tunnel = tunnel + self.tunnelReceiptURL = tunnelReceiptURL subscriptions = [] } } @@ -84,12 +88,14 @@ private extension AppContext { func onLaunch() async throws { pp_log(.app, .notice, "Application did launch") - pp_log(.App.profiles, .info, "Read and observe local profiles...") + pp_log(.App.profiles, .info, "\tRead and observe local profiles...") try await profileManager.observeLocal() + pp_log(.App.profiles, .info, "\tObserve in-app events...") iapManager.observeObjects() await iapManager.reloadReceipt() + pp_log(.App.profiles, .info, "\tObserve eligible features...") iapManager .$eligibleFeatures .removeDuplicates() @@ -100,6 +106,7 @@ private extension AppContext { } .store(in: &subscriptions) + pp_log(.App.profiles, .info, "\tObserve changes in ProfileManager...") profileManager .didChange .sink { [weak self] event in @@ -117,21 +124,20 @@ private extension AppContext { // copy release receipt to tunnel for TestFlight eligibility (once is enough, it won't change) if let appReceiptURL = Bundle.main.appStoreProductionReceiptURL { - let tunnelReceiptURL = BundleConfiguration.urlForBetaReceipt do { - pp_log(.App.iap, .info, "Copy release receipt to tunnel...") + pp_log(.App.iap, .info, "\tCopy release receipt to tunnel...") try? FileManager.default.removeItem(at: tunnelReceiptURL) try FileManager.default.copyItem(at: appReceiptURL, to: tunnelReceiptURL) } catch { - pp_log(.App.iap, .error, "Unable to copy release receipt to tunnel: \(error)") + pp_log(.App.iap, .error, "\tUnable to copy release receipt to tunnel: \(error)") } } do { - pp_log(.app, .notice, "Fetch providers index...") + pp_log(.app, .info, "\tFetch providers index...") try await providerManager.fetchIndex(from: API.shared) } catch { - pp_log(.app, .error, "Unable to fetch providers index: \(error)") + pp_log(.app, .error, "\tUnable to fetch providers index: \(error)") } } @@ -144,10 +150,10 @@ private extension AppContext { pp_log(.app, .notice, "Application did enter foreground") pendingTask = Task { do { - pp_log(.App.profiles, .info, "Refresh local profiles observers...") + pp_log(.App.profiles, .info, "\tRefresh local profiles observers...") try await profileManager.observeLocal() } catch { - pp_log(.App.profiles, .error, "Unable to re-observe local profiles: \(error)") + pp_log(.App.profiles, .error, "\tUnable to re-observe local profiles: \(error)") } await iapManager.reloadReceipt() @@ -165,10 +171,10 @@ private extension AppContext { // toggle sync based on .sharing eligibility let isEligibleForSharing = features.contains(.sharing) do { - pp_log(.App.profiles, .info, "Refresh remote profiles observers (eligible=\(isEligibleForSharing), CloudKit=\(isCloudKitEnabled))...") + pp_log(.App.profiles, .info, "\tRefresh remote profiles observers (eligible=\(isEligibleForSharing), CloudKit=\(isCloudKitEnabled))...") try await profileManager.observeRemote(isEligibleForSharing && isCloudKitEnabled) } catch { - pp_log(.App.profiles, .error, "Unable to re-observe remote profiles: \(error)") + pp_log(.App.profiles, .error, "\tUnable to re-observe remote profiles: \(error)") } } await pendingTask?.value @@ -180,29 +186,29 @@ private extension AppContext { pp_log(.app, .notice, "Application did save profile (\(profile.id))") guard profile.id == tunnel.currentProfile?.id else { - pp_log(.app, .debug, "Profile \(profile.id) is not current, do nothing") + pp_log(.app, .debug, "\tProfile \(profile.id) is not current, do nothing") return } guard [.active, .activating].contains(tunnel.status) else { - pp_log(.app, .debug, "Connection is not active (\(tunnel.status)), do nothing") + pp_log(.app, .debug, "\tConnection is not active (\(tunnel.status)), do nothing") return } pendingTask = Task { do { if profile.isInteractive { - pp_log(.app, .info, "Profile \(profile.id) is interactive, disconnect") + pp_log(.app, .info, "\tProfile \(profile.id) is interactive, disconnect") try await tunnel.disconnect() return } do { - pp_log(.app, .info, "Reconnect profile \(profile.id)") + pp_log(.app, .info, "\tReconnect profile \(profile.id)") try await tunnel.connect(with: profile) } catch { - pp_log(.app, .error, "Unable to reconnect profile \(profile.id), disconnect: \(error)") + pp_log(.app, .error, "\tUnable to reconnect profile \(profile.id), disconnect: \(error)") try await tunnel.disconnect() } } catch { - pp_log(.app, .error, "Unable to reinstate connection on save profile \(profile.id): \(error)") + pp_log(.app, .error, "\tUnable to reinstate connection on save profile \(profile.id): \(error)") } } await pendingTask?.value diff --git a/Passepartout/Library/Sources/UILibrary/Mock/AppContext+Mock.swift b/Passepartout/Library/Sources/UILibrary/Mock/AppContext+Mock.swift index 15f9dce4..d21dd644 100644 --- a/Passepartout/Library/Sources/UILibrary/Mock/AppContext+Mock.swift +++ b/Passepartout/Library/Sources/UILibrary/Mock/AppContext+Mock.swift @@ -83,7 +83,8 @@ extension AppContext { profileManager: profileManager, providerManager: providerManager, registry: registry, - tunnel: tunnel + tunnel: tunnel, + tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt ) } } diff --git a/Passepartout/Shared/AppContext+Shared.swift b/Passepartout/Shared/AppContext+Shared.swift index e83cca85..daf0e151 100644 --- a/Passepartout/Shared/AppContext+Shared.swift +++ b/Passepartout/Shared/AppContext+Shared.swift @@ -87,10 +87,7 @@ extension AppContext { cloudKitIdentifier: nil, author: nil ) - let repository = AppData.cdProviderRepositoryV3( - context: store.context, - backgroundContext: store.backgroundContext - ) + let repository = AppData.cdProviderRepositoryV3(context: store.backgroundContext) return ProviderManager(repository: repository) }() @@ -119,7 +116,8 @@ extension AppContext { profileManager: profileManager, providerManager: providerManager, registry: .shared, - tunnel: tunnel + tunnel: tunnel, + tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt ) }() }