mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-01-22 08:32:11 +00:00
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:
parent
26e97625fa
commit
f8e623e1fe
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user