Fix regressions with CloudKit synchronization (#1029)

The remote container is shared by ProfileManager and
PreferencesManager, but it must be the same for CloudKit sync
to work properly.

Externalize the logic of onEligibleFeatures() so that the
AppContext singleton can update the managers (and their
repositories) with the new remote store.

Now that the remote profile repository is reloaded every time that
eligible features change, the .removeDuplicates() may also be
restored. Just add a .dropFirst() to skip the initially empty
value of eligible features. Even when features are eventually empty,
a value is always emitted after IAPManager.reloadReceipt()

Lastly, enable Core Data lightweight migration.

Regressions from #1017
This commit is contained in:
Davide 2024-12-20 10:05:07 +01:00 committed by GitHub
parent 26e97625fa
commit f8e623e1fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 191 additions and 224 deletions

View File

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

View File

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

View File

@ -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<AppFeature>]
@Published
public private(set) var isRemoteImportingEnabled: Bool
public var isRemoteImportingEnabled = false
private var waitingObservers: Set<Observer> {
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)

View File

@ -90,6 +90,10 @@ public final class CoreDataPersistentStore: Sendable {
// container was formerly created with CloudKit option
desc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
// migrate automatically
desc.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
desc.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
// report remote notifications (do this BEFORE loadPersistentStores)
//
// https://stackoverflow.com/a/69507329/784615

View File

@ -180,20 +180,19 @@ private extension CoreDataRepository {
request.predicate = predicate
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
}
let newController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: self.context,
sectionNameKeyPath: nil,
cacheName: nil
)
newController.delegate = self
resultsController = newController
return await sendResults(from: newController)
return try await context.perform {
try newController.performFetch()
return self.unsafeSendResults(from: newController)
}
}
@discardableResult

View File

@ -48,6 +48,8 @@ public final class AppContext: ObservableObject, Sendable {
private let tunnelReceiptURL: URL?
private let onEligibleFeaturesBlock: ((Set<AppFeature>) async -> Void)?
private var launchTask: Task<Void, Error>?
private var pendingTask: Task<Void, Never>?
@ -62,7 +64,8 @@ public final class AppContext: ObservableObject, Sendable {
preferencesManager: PreferencesManager,
registry: Registry,
tunnel: ExtendedTunnel,
tunnelReceiptURL: URL?
tunnelReceiptURL: URL?,
onEligibleFeaturesBlock: ((Set<AppFeature>) 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
}
}

View File

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

View File

@ -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<AppFeature>) 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: {

View File

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

View File

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