diff --git a/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift b/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift index 9a0a51706..dc26b131e 100644 --- a/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift +++ b/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift @@ -212,7 +212,7 @@ private extension ProfileRowView { } .task { do { - try await profileManager.observeRemote(true) + try await profileManager.observeRemote(repository: InMemoryProfileRepository()) try await profileManager.save(profile, isLocal: true, remotelyShared: true) } catch { fatalError(error.localizedDescription) diff --git a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift index 803187026..84b6dfe45 100644 --- a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift +++ b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift @@ -29,18 +29,15 @@ import PassepartoutKit @MainActor public final class PreferencesManager: ObservableObject { - private let modulesFactory: (UUID) throws -> ModulePreferencesRepository + public var modulesRepositoryFactory: (UUID) throws -> ModulePreferencesRepository - private let providersFactory: (ProviderID) throws -> ProviderPreferencesRepository + public var providersRepositoryFactory: (ProviderID) throws -> ProviderPreferencesRepository - public init( - modulesFactory: ((UUID) throws -> ModulePreferencesRepository)? = nil, - providersFactory: ((ProviderID) throws -> ProviderPreferencesRepository)? = nil - ) { - self.modulesFactory = modulesFactory ?? { _ in + public init() { + modulesRepositoryFactory = { _ in DummyModulePreferencesRepository() } - self.providersFactory = providersFactory ?? { _ in + providersRepositoryFactory = { _ in DummyProviderPreferencesRepository() } } @@ -48,11 +45,11 @@ public final class PreferencesManager: ObservableObject { extension PreferencesManager { public func preferencesRepository(forModuleWithId moduleId: UUID) throws -> ModulePreferencesRepository { - try modulesFactory(moduleId) + try modulesRepositoryFactory(moduleId) } public func preferencesRepository(forProviderWithId providerId: ProviderID) throws -> ProviderPreferencesRepository { - try providersFactory(providerId) + try providersRepositoryFactory(providerId) } } diff --git a/Library/Sources/CommonLibrary/Business/ProfileManager.swift b/Library/Sources/CommonLibrary/Business/ProfileManager.swift index 28db6f3ec..048e47dfc 100644 --- a/Library/Sources/CommonLibrary/Business/ProfileManager.swift +++ b/Library/Sources/CommonLibrary/Business/ProfileManager.swift @@ -59,8 +59,6 @@ public final class ProfileManager: ObservableObject { private let backupRepository: ProfileRepository? - private let remoteRepositoryBlock: ((Bool) -> ProfileRepository)? - private var remoteRepository: ProfileRepository? private let mirrorsRemoteRepository: Bool @@ -94,7 +92,7 @@ public final class ProfileManager: ObservableObject { private var requiredFeatures: [Profile.ID: Set] @Published - public private(set) var isRemoteImportingEnabled: Bool + public var isRemoteImportingEnabled = false private var waitingObservers: Set { didSet { @@ -120,34 +118,25 @@ public final class ProfileManager: ObservableObject { // for testing/previews public convenience init(profiles: [Profile]) { - self.init( - repository: InMemoryProfileRepository(profiles: profiles), - remoteRepositoryBlock: { _ in - InMemoryProfileRepository() - } - ) + self.init(repository: InMemoryProfileRepository(profiles: profiles)) } public init( + processor: ProfileProcessor? = nil, repository: ProfileRepository, backupRepository: ProfileRepository? = nil, - remoteRepositoryBlock: ((Bool) -> ProfileRepository)?, - mirrorsRemoteRepository: Bool = false, - processor: ProfileProcessor? = nil + mirrorsRemoteRepository: Bool = false ) { - precondition(!mirrorsRemoteRepository || remoteRepositoryBlock != nil, "mirrorsRemoteRepository requires a non-nil remoteRepositoryBlock") + self.processor = processor self.repository = repository self.backupRepository = backupRepository - self.remoteRepositoryBlock = remoteRepositoryBlock self.mirrorsRemoteRepository = mirrorsRemoteRepository - self.processor = processor allProfiles = [:] allRemoteProfiles = [:] filteredProfiles = [] requiredFeatures = [:] - isRemoteImportingEnabled = false - if remoteRepositoryBlock != nil { + if mirrorsRemoteRepository { waitingObservers = [.local, .remote] } else { waitingObservers = [.local] @@ -341,24 +330,13 @@ extension ProfileManager { } } - public func observeRemote(_ isRemoteImportingEnabled: Bool) async throws { - guard let remoteRepositoryBlock else { -// preconditionFailure("Missing remoteRepositoryBlock") - return - } - guard remoteRepository == nil || isRemoteImportingEnabled != self.isRemoteImportingEnabled else { - return - } - - self.isRemoteImportingEnabled = isRemoteImportingEnabled - + public func observeRemote(repository: ProfileRepository) async throws { remoteSubscription = nil - let newRepository = remoteRepositoryBlock(isRemoteImportingEnabled) - let initialProfiles = try await newRepository.fetchProfiles() + remoteRepository = repository + let initialProfiles = try await repository.fetchProfiles() reloadRemoteProfiles(initialProfiles) - remoteRepository = newRepository - remoteSubscription = remoteRepository? + remoteSubscription = repository .profilesPublisher .dropFirst() .receive(on: DispatchQueue.main) @@ -422,6 +400,7 @@ private extension ProfileManager { if waitingObservers.contains(.remote) { waitingObservers.remove(.remote) } + Task { [weak self] in self?.didChange.send(.startRemoteImport) await self?.importRemoteProfiles(result) diff --git a/Library/Sources/UILibrary/Business/AppContext.swift b/Library/Sources/UILibrary/Business/AppContext.swift index 0000cc09c..5739e95ce 100644 --- a/Library/Sources/UILibrary/Business/AppContext.swift +++ b/Library/Sources/UILibrary/Business/AppContext.swift @@ -48,6 +48,8 @@ public final class AppContext: ObservableObject, Sendable { private let tunnelReceiptURL: URL? + private let onEligibleFeaturesBlock: ((Set) async -> Void)? + private var launchTask: Task? private var pendingTask: Task? @@ -62,7 +64,8 @@ public final class AppContext: ObservableObject, Sendable { preferencesManager: PreferencesManager, registry: Registry, tunnel: ExtendedTunnel, - tunnelReceiptURL: URL? + tunnelReceiptURL: URL?, + onEligibleFeaturesBlock: ((Set) async -> Void)? = nil ) { self.iapManager = iapManager self.migrationManager = migrationManager @@ -72,6 +75,7 @@ public final class AppContext: ObservableObject, Sendable { self.registry = registry self.tunnel = tunnel self.tunnelReceiptURL = tunnelReceiptURL + self.onEligibleFeaturesBlock = onEligibleFeaturesBlock subscriptions = [] } } @@ -99,22 +103,16 @@ private extension AppContext { pp_log(.App.profiles, .info, "\tObserve in-app events...") iapManager.observeObjects() - // load in background, see comment right below + // defer load receipt Task { await iapManager.reloadReceipt() } - // using Task above (#1019) causes the receipt to be loaded asynchronously. - // the initial call to onEligibleFeatures() may execute before the receipt is - // loaded and therefore do nothing. with .removeDuplicates(), there would - // not be a second chance to call onEligibleFeatures() if the eligible - // features haven't changed after reloading the receipt (this is the case - // for TestFlight where some features are set statically). that's why it's - // commented now pp_log(.App.profiles, .info, "\tObserve eligible features...") iapManager .$eligibleFeatures -// .removeDuplicates() + .dropFirst() + .removeDuplicates() .sink { [weak self] eligible in Task { try await self?.onEligibleFeatures(eligible) @@ -184,19 +182,7 @@ private extension AppContext { pp_log(.app, .notice, "Application did update eligible features") pendingTask = Task { - - // toggle sync based on .sharing eligibility - let isEligibleForSharing = features.contains(.sharing) - do { - 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, "\tUnable to re-observe remote profiles: \(error)") - } - - // refresh required profile features - pp_log(.App.profiles, .info, "\tReload profiles required features...") - profileManager.reloadRequiredFeatures() + await onEligibleFeaturesBlock?(features) } await pendingTask?.value pendingTask = nil @@ -262,18 +248,3 @@ private extension AppContext { return didLaunch } } - -// MARK: - Helpers - -private extension AppContext { - var isCloudKitEnabled: Bool { -#if os(tvOS) - true -#else - if AppCommandLine.contains(.uiTesting) { - return true - } - return FileManager.default.ubiquityIdentityToken != nil -#endif - } -} diff --git a/Library/Tests/CommonLibraryTests/Business/ProfileManagerTests.swift b/Library/Tests/CommonLibraryTests/Business/ProfileManagerTests.swift index 59d5300d1..73e75e2da 100644 --- a/Library/Tests/CommonLibraryTests/Business/ProfileManagerTests.swift +++ b/Library/Tests/CommonLibraryTests/Business/ProfileManagerTests.swift @@ -50,7 +50,7 @@ extension ProfileManagerTests { func test_givenRepository_whenNotReady_thenHasNoProfiles() { let repository = InMemoryProfileRepository(profiles: []) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil) + let sut = ProfileManager(repository: repository) XCTAssertFalse(sut.isReady) XCTAssertFalse(sut.hasProfiles) XCTAssertTrue(sut.previews.isEmpty) @@ -59,7 +59,7 @@ extension ProfileManagerTests { func test_givenRepository_whenReady_thenHasProfiles() async throws { let profile = newProfile() let repository = InMemoryProfileRepository(profiles: [profile]) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil) + let sut = ProfileManager(repository: repository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -72,7 +72,7 @@ extension ProfileManagerTests { let profile1 = newProfile("foo") let profile2 = newProfile("bar") let repository = InMemoryProfileRepository(profiles: [profile1, profile2]) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil) + let sut = ProfileManager(repository: repository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -93,7 +93,7 @@ extension ProfileManagerTests { let repository = InMemoryProfileRepository(profiles: [profile]) let processor = MockProfileProcessor() processor.requiredFeatures = [.appleTV, .onDemand] - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil, processor: processor) + let sut = ProfileManager(processor: processor, repository: repository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -115,7 +115,7 @@ extension ProfileManagerTests { processor.isIncludedBlock = { $0.name == "local2" } - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil, processor: processor) + let sut = ProfileManager(processor: processor, repository: repository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -129,7 +129,7 @@ extension ProfileManagerTests { let repository = InMemoryProfileRepository(profiles: [profile]) let processor = MockProfileProcessor() processor.requiredFeatures = [.appleTV, .onDemand] - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil, processor: processor) + let sut = ProfileManager(processor: processor, repository: repository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -155,7 +155,7 @@ extension ProfileManagerTests { extension ProfileManagerTests { func test_givenRepository_whenSave_thenIsSaved() async throws { let repository = InMemoryProfileRepository(profiles: []) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil) + let sut = ProfileManager(repository: repository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -172,7 +172,7 @@ extension ProfileManagerTests { func test_givenRepository_whenSaveExisting_thenIsReplaced() async throws { let profile = newProfile("oldName") let repository = InMemoryProfileRepository(profiles: [profile]) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil) + let sut = ProfileManager(repository: repository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -191,7 +191,7 @@ extension ProfileManagerTests { func test_givenRepositoryAndProcessor_whenSave_thenProcessorIsNotInvoked() async throws { let repository = InMemoryProfileRepository(profiles: []) let processor = MockProfileProcessor() - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil, processor: processor) + let sut = ProfileManager(processor: processor, repository: repository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -207,7 +207,7 @@ extension ProfileManagerTests { func test_givenRepositoryAndProcessor_whenSaveLocal_thenProcessorIsInvoked() async throws { let repository = InMemoryProfileRepository(profiles: []) let processor = MockProfileProcessor() - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil, processor: processor) + let sut = ProfileManager(processor: processor, repository: repository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -221,7 +221,7 @@ extension ProfileManagerTests { func test_givenRepository_whenSave_thenIsStoredToBackUpRepository() async throws { let repository = InMemoryProfileRepository(profiles: []) let backupRepository = InMemoryProfileRepository(profiles: []) - let sut = ProfileManager(repository: repository, backupRepository: backupRepository, remoteRepositoryBlock: nil) + let sut = ProfileManager(repository: repository, backupRepository: backupRepository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -247,7 +247,7 @@ extension ProfileManagerTests { func test_givenRepository_whenRemove_thenIsRemoved() async throws { let profile = newProfile() let repository = InMemoryProfileRepository(profiles: [profile]) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil) + let sut = ProfileManager(repository: repository) try await waitForReady(sut) XCTAssertTrue(sut.isReady) @@ -267,11 +267,9 @@ extension ProfileManagerTests { let profile = newProfile() let repository = InMemoryProfileRepository() let remoteRepository = InMemoryProfileRepository() - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in - remoteRepository - }) + let sut = ProfileManager(repository: repository) - try await waitForReady(sut) + try await waitForReady(sut, remoteRepository: remoteRepository) let exp = expectation(description: "Remote") remoteRepository @@ -291,15 +289,13 @@ extension ProfileManagerTests { XCTAssertTrue(sut.isRemotelyShared(profileWithId: profile.id)) } - func test_givenRemoteRepository_whenSaveNotRemotelyShared_thenIsRemoveFromRemoteRepository() async throws { + func test_givenRemoteRepository_whenSaveNotRemotelyShared_thenIsRemovedFromRemoteRepository() async throws { let profile = newProfile() let repository = InMemoryProfileRepository(profiles: [profile]) let remoteRepository = InMemoryProfileRepository(profiles: [profile]) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in - remoteRepository - }) + let sut = ProfileManager(repository: repository) - try await waitForReady(sut) + try await waitForReady(sut, remoteRepository: remoteRepository) let exp = expectation(description: "Remote") remoteRepository @@ -325,7 +321,7 @@ extension ProfileManagerTests { func test_givenRepository_whenNew_thenReturnsProfileWithNewName() async throws { let profile = newProfile("example") let repository = InMemoryProfileRepository(profiles: [profile]) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil) + let sut = ProfileManager(repository: repository) try await waitForReady(sut) XCTAssertEqual(sut.previews.count, 1) @@ -337,7 +333,7 @@ extension ProfileManagerTests { func test_givenRepository_whenDuplicate_thenSavesProfileWithNewName() async throws { let profile = newProfile("example") let repository = InMemoryProfileRepository(profiles: [profile]) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil) + let sut = ProfileManager(repository: repository) try await waitForReady(sut) @@ -381,13 +377,11 @@ extension ProfileManagerTests { let allProfiles = localProfiles + remoteProfiles let repository = InMemoryProfileRepository(profiles: localProfiles) let remoteRepository = InMemoryProfileRepository(profiles: remoteProfiles) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in - remoteRepository - }) + let sut = ProfileManager(repository: repository) try await wait(sut, "Remote import", until: .stopRemoteImport) { try await $0.observeLocal() - try await $0.observeRemote(true) + try await $0.observeRemote(repository: remoteRepository) } XCTAssertEqual(sut.previews.count, allProfiles.count) @@ -417,13 +411,11 @@ extension ProfileManagerTests { ] let repository = InMemoryProfileRepository(profiles: localProfiles) let remoteRepository = InMemoryProfileRepository(profiles: remoteProfiles) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in - remoteRepository - }) + let sut = ProfileManager(repository: repository) try await wait(sut, "Remote import", until: .stopRemoteImport) { try await $0.observeLocal() - try await $0.observeRemote(true) + try await $0.observeRemote(repository: remoteRepository) } XCTAssertEqual(sut.previews.count, 4) // unique IDs @@ -464,13 +456,11 @@ extension ProfileManagerTests { processor.isIncludedBlock = { !$0.name.hasPrefix("remote") } - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in - remoteRepository - }, processor: processor) + let sut = ProfileManager(processor: processor, repository: repository) try await wait(sut, "Remote import", until: .stopRemoteImport) { try await $0.observeLocal() - try await $0.observeRemote(true) + try await $0.observeRemote(repository: remoteRepository) } XCTAssertEqual(processor.isIncludedCount, allProfiles.count) @@ -499,13 +489,11 @@ extension ProfileManagerTests { let repository = InMemoryProfileRepository(profiles: localProfiles) let remoteRepository = InMemoryProfileRepository(profiles: remoteProfiles) let processor = MockProfileProcessor() - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in - remoteRepository - }, processor: processor) + let sut = ProfileManager(processor: processor, repository: repository) try await wait(sut, "Remote import", until: .stopRemoteImport) { try await $0.observeLocal() - try await $0.observeRemote(true) + try await $0.observeRemote(repository: remoteRepository) } try sut.previews.forEach { @@ -531,13 +519,11 @@ extension ProfileManagerTests { ] let repository = InMemoryProfileRepository(profiles: localProfiles) let remoteRepository = InMemoryProfileRepository() - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in - remoteRepository - }) + let sut = ProfileManager(repository: repository) try await wait(sut, "Remote import", until: .stopRemoteImport) { try await $0.observeLocal() - try await $0.observeRemote(true) + try await $0.observeRemote(repository: remoteRepository) } XCTAssertEqual(sut.previews.count, localProfiles.count) @@ -589,13 +575,11 @@ extension ProfileManagerTests { let remoteProfiles = [profile] let repository = InMemoryProfileRepository(profiles: localProfiles) let remoteRepository = InMemoryProfileRepository(profiles: remoteProfiles) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in - remoteRepository - }) + let sut = ProfileManager(repository: repository) try await wait(sut, "Remote import", until: .stopRemoteImport) { try await $0.observeLocal() - try await $0.observeRemote(true) + try await $0.observeRemote(repository: remoteRepository) } XCTAssertEqual(sut.previews.count, 1) @@ -611,13 +595,11 @@ extension ProfileManagerTests { let localProfiles = [profile] let repository = InMemoryProfileRepository(profiles: localProfiles) let remoteRepository = InMemoryProfileRepository(profiles: localProfiles) - let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in - remoteRepository - }, mirrorsRemoteRepository: true) + let sut = ProfileManager(repository: repository, mirrorsRemoteRepository: true) try await wait(sut, "Remote import", until: .stopRemoteImport) { try await $0.observeLocal() - try await $0.observeRemote(true) + try await $0.observeRemote(repository: remoteRepository) } XCTAssertEqual(sut.previews.count, 1) @@ -644,10 +626,12 @@ private extension ProfileManagerTests { } } - func waitForReady(_ sut: ProfileManager, importingRemote: Bool = true) async throws { + func waitForReady(_ sut: ProfileManager, remoteRepository: ProfileRepository? = nil) async throws { try await wait(sut, "Ready", until: .ready) { try await $0.observeLocal() - try await $0.observeRemote(importingRemote) + if let remoteRepository { + try await $0.observeRemote(repository: remoteRepository) + } } } diff --git a/Passepartout/App/Context/AppContext+Shared.swift b/Passepartout/App/Context/AppContext+Shared.swift index be338e1c5..a38d67c1f 100644 --- a/Passepartout/App/Context/AppContext+Shared.swift +++ b/Passepartout/App/Context/AppContext+Shared.swift @@ -40,18 +40,39 @@ extension AppContext { static let shared: AppContext = { let dependencies: Dependencies = .shared + // MARK: Core Data + + guard let cdLocalModel = NSManagedObjectModel.mergedModel(from: [ + AppData.providersBundle + ]) else { + fatalError("Unable to load local model") + } guard let cdRemoteModel = NSManagedObjectModel.mergedModel(from: [ AppData.profilesBundle, AppData.preferencesBundle ]) else { fatalError("Unable to load remote model") } - guard let cdLocalModel = NSManagedObjectModel.mergedModel(from: [ - AppData.providersBundle - ]) else { - fatalError("Unable to load local model") + + let localStore = CoreDataPersistentStore( + logger: dependencies.coreDataLogger(), + containerName: Constants.shared.containers.local, + model: cdLocalModel, + cloudKitIdentifier: nil, + author: nil + ) + let newRemoteStore: (_ cloudKit: Bool) -> CoreDataPersistentStore = { + CoreDataPersistentStore( + logger: dependencies.coreDataLogger(), + containerName: Constants.shared.containers.remote, + model: cdRemoteModel, + cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil, + author: nil + ) } + // MARK: Managers + let iapManager = IAPManager( customUserLevel: dependencies.customUserLevel, inAppHelper: dependencies.simulatedAppProductHelper(), @@ -59,13 +80,13 @@ extension AppContext { betaChecker: dependencies.betaChecker(), productsAtBuild: dependencies.productsAtBuild() ) - let processor = dependencies.appProcessor(with: iapManager) - let tunnelEnvironment = dependencies.tunnelEnvironment() + let tunnelReceiptURL = BundleConfiguration.urlForBetaReceipt + let tunnelEnvironment = dependencies.tunnelEnvironment() #if targetEnvironment(simulator) let tunnelStrategy = FakeTunnelStrategy(environment: tunnelEnvironment, dataCountInterval: 1000) - let mainProfileRepository = dependencies.coreDataProfileRepository( + let mainProfileRepository = dependencies.backupProfileRepository( model: cdRemoteModel, observingResults: true ) @@ -80,36 +101,15 @@ extension AppContext { } #endif - let profileManager: ProfileManager = { - let remoteRepositoryBlock: (Bool) -> ProfileRepository = { - let remoteStore = CoreDataPersistentStore( - logger: dependencies.coreDataLogger(), - containerName: Constants.shared.containers.remote, - model: cdRemoteModel, - cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil, - author: nil - ) - return AppData.cdProfileRepositoryV3( - registry: dependencies.registry, - coder: CodableProfileCoder(), - context: remoteStore.context, - observingResults: true, - onResultError: { - pp_log(.App.profiles, .error, "Unable to decode remote profile: \($0)") - return .ignore - } - ) - } - return ProfileManager( - repository: mainProfileRepository, - backupRepository: dependencies.backupProfileRepository( - model: cdRemoteModel - ), - remoteRepositoryBlock: remoteRepositoryBlock, - mirrorsRemoteRepository: dependencies.mirrorsRemoteRepository, - processor: processor - ) - }() + let profileManager = ProfileManager( + processor: processor, + repository: mainProfileRepository, + backupRepository: dependencies.backupProfileRepository( + model: cdRemoteModel, + observingResults: false + ), + mirrorsRemoteRepository: dependencies.mirrorsRemoteRepository + ) let tunnel = ExtendedTunnel( tunnel: Tunnel(strategy: tunnelStrategy), @@ -119,14 +119,7 @@ extension AppContext { ) let providerManager: ProviderManager = { - let store = CoreDataPersistentStore( - logger: dependencies.coreDataLogger(), - containerName: Constants.shared.containers.local, - model: cdLocalModel, - cloudKitIdentifier: nil, - author: nil - ) - let repository = AppData.cdProviderRepositoryV3(context: store.backgroundContext()) + let repository = AppData.cdProviderRepositoryV3(context: localStore.backgroundContext()) return ProviderManager(repository: repository) }() @@ -155,29 +148,61 @@ extension AppContext { return MigrationManager(profileStrategy: profileStrategy, simulation: migrationSimulation) }() - let preferencesManager: PreferencesManager = { - let preferencesStore = CoreDataPersistentStore( - logger: dependencies.coreDataLogger(), - containerName: Constants.shared.containers.remote, - model: cdRemoteModel, - cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitId), - author: nil - ) - return PreferencesManager( - modulesFactory: { + let preferencesManager = PreferencesManager() + + // MARK: Eligibility + + let onEligibleFeaturesBlock: (Set) async -> Void = { @MainActor features in + let isEligibleForSharing = features.contains(.sharing) + let isRemoteImportingEnabled = isEligibleForSharing && isCloudKitEnabled + + // toggle CloudKit sync based on .sharing eligibility + let remoteStore = newRemoteStore(isRemoteImportingEnabled) + + // @Published + profileManager.isRemoteImportingEnabled = isRemoteImportingEnabled + + do { + pp_log(.app, .info, "\tRefresh remote sync (eligible=\(isEligibleForSharing), CloudKit=\(isCloudKitEnabled))...") + + pp_log(.App.profiles, .info, "\tRefresh remote profiles repository (sync=\(isRemoteImportingEnabled))...") + try await profileManager.observeRemote(repository: { + AppData.cdProfileRepositoryV3( + registry: dependencies.registry, + coder: dependencies.profileCoder(), + context: remoteStore.context, + observingResults: true, + onResultError: { + pp_log(.App.profiles, .error, "Unable to decode remote profile: \($0)") + return .ignore + } + ) + }()) + + pp_log(.app, .info, "\tRefresh modules preferences repository...") + preferencesManager.modulesRepositoryFactory = { try AppData.cdModulePreferencesRepositoryV3( - context: preferencesStore.context, + context: remoteStore.context, moduleId: $0 ) - }, - providersFactory: { + } + + pp_log(.app, .info, "\tRefresh providers preferences repository...") + preferencesManager.providersRepositoryFactory = { try AppData.cdProviderPreferencesRepositoryV3( - context: preferencesStore.context, + context: remoteStore.context, providerId: $0 ) } - ) - }() + } catch { + pp_log(.App.profiles, .error, "\tUnable to re-observe remote profiles: \(error)") + } + + pp_log(.App.profiles, .info, "\tReload profiles required features...") + profileManager.reloadRequiredFeatures() + } + + // MARK: Build return AppContext( iapManager: iapManager, @@ -187,11 +212,25 @@ extension AppContext { preferencesManager: preferencesManager, registry: dependencies.registry, tunnel: tunnel, - tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt + tunnelReceiptURL: tunnelReceiptURL, + onEligibleFeaturesBlock: onEligibleFeaturesBlock ) }() } +private extension AppContext { + static var isCloudKitEnabled: Bool { +#if os(tvOS) + true +#else + if AppCommandLine.contains(.uiTesting) { + return true + } + return FileManager.default.ubiquityIdentityToken != nil +#endif + } +} + // MARK: - Dependencies private extension Dependencies { @@ -237,15 +276,7 @@ private extension Dependencies { #endif } - func backupProfileRepository(model: NSManagedObjectModel) -> ProfileRepository? { -#if targetEnvironment(simulator) - nil -#else - coreDataProfileRepository(model: model, observingResults: false) -#endif - } - - func coreDataProfileRepository(model: NSManagedObjectModel, observingResults: Bool) -> ProfileRepository { + func backupProfileRepository(model: NSManagedObjectModel, observingResults: Bool) -> ProfileRepository { let store = CoreDataPersistentStore( logger: coreDataLogger(), containerName: Constants.shared.containers.backup, @@ -255,7 +286,7 @@ private extension Dependencies { ) return AppData.cdProfileRepositoryV3( registry: registry, - coder: CodableProfileCoder(), + coder: profileCoder(), context: store.context, observingResults: observingResults, onResultError: { diff --git a/Passepartout/App/Context/ProfileManager+Testing.swift b/Passepartout/App/Context/ProfileManager+Testing.swift index e083673c0..4ad82aaf7 100644 --- a/Passepartout/App/Context/ProfileManager+Testing.swift +++ b/Passepartout/App/Context/ProfileManager+Testing.swift @@ -31,14 +31,12 @@ extension ProfileManager { public static func forUITesting(withRegistry registry: Registry, processor: ProfileProcessor) -> ProfileManager { let repository = InMemoryProfileRepository() let remoteRepository = InMemoryProfileRepository() - let manager = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in - remoteRepository - }, processor: processor) + let manager = ProfileManager(processor: processor, repository: repository) Task { do { try await manager.observeLocal() - try await manager.observeRemote(true) + try await manager.observeRemote(repository: remoteRepository) for parameters in mockParameters { var builder = Profile.Builder() diff --git a/Passepartout/Shared/Dependencies+PassepartoutKit.swift b/Passepartout/Shared/Dependencies+PassepartoutKit.swift index 77afc1925..7b9b70a8e 100644 --- a/Passepartout/Shared/Dependencies+PassepartoutKit.swift +++ b/Passepartout/Shared/Dependencies+PassepartoutKit.swift @@ -33,11 +33,15 @@ extension Dependencies { Self.sharedRegistry } + func profileCoder() -> ProfileCoder { + CodableProfileCoder() + } + func neProtocolCoder() -> NEProtocolCoder { KeychainNEProtocolCoder( tunnelBundleIdentifier: BundleConfiguration.mainString(for: .tunnelId), registry: registry, - coder: CodableProfileCoder(), + coder: profileCoder(), keychain: AppleKeychain(group: BundleConfiguration.mainString(for: .keychainGroupId)) ) }