diff --git a/CHANGELOG.md b/CHANGELOG.md index 55bea7927..a03713b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- iCloud Sync between the iPhone and Mac apps: the iOS app now uses the Production CloudKit environment, so a development build no longer syncs into a separate database the Mac never reads. + ## [0.50.0] - 2026-06-09 ### Added diff --git a/TableProMobile/TableProMobile/TableProMobileRelease.entitlements b/TableProMobile/TableProMobile/TableProMobileRelease.entitlements index f8880a803..3ae2bfe54 100644 --- a/TableProMobile/TableProMobile/TableProMobileRelease.entitlements +++ b/TableProMobile/TableProMobile/TableProMobileRelease.entitlements @@ -10,6 +10,8 @@ CloudKit + com.apple.developer.icloud-container-environment + Production com.apple.security.application-groups group.com.TablePro.TableProMobile diff --git a/TableProTests/Core/Sync/EntitlementsEnvironmentParityTests.swift b/TableProTests/Core/Sync/EntitlementsEnvironmentParityTests.swift new file mode 100644 index 000000000..3fee5a9d9 --- /dev/null +++ b/TableProTests/Core/Sync/EntitlementsEnvironmentParityTests.swift @@ -0,0 +1,53 @@ +// +// EntitlementsEnvironmentParityTests.swift +// TableProTests +// +// Guards the CloudKit environment pin. The Mac and iOS apps must both target +// the Production environment, or a development iOS build talks to the +// Development database while the Mac talks to Production and sync silently +// moves nothing across devices. +// + +import Foundation +import Testing + +@Suite("CloudKit environment entitlement parity") +struct EntitlementsEnvironmentParityTests { + private static let environmentKey = "com.apple.developer.icloud-container-environment" + private static let macEntitlements = "TablePro/TablePro.entitlements" + private static let iosEntitlements = "TableProMobile/TableProMobile/TableProMobileRelease.entitlements" + + @Test("Mac app pins the Production CloudKit environment") + func macTargetsProduction() throws { + try #expect(environment(in: Self.macEntitlements) == "Production") + } + + @Test("iOS app pins the Production CloudKit environment") + func iosTargetsProduction() throws { + try #expect(environment(in: Self.iosEntitlements) == "Production") + } + + private func environment(in relativePath: String) throws -> String? { + let url = try repoRoot().appendingPathComponent(relativePath) + let data = try Data(contentsOf: url) + let plist = try PropertyListSerialization.propertyList(from: data, format: nil) + let entitlements = try #require(plist as? [String: Any], "Entitlements at \(relativePath) is not a dictionary") + return entitlements[Self.environmentKey] as? String + } + + private func repoRoot(file: StaticString = #filePath) throws -> URL { + var directory = URL(fileURLWithPath: "\(file)").deletingLastPathComponent() + while directory.path != "/" { + let marker = directory.appendingPathComponent("TablePro.xcodeproj") + if FileManager.default.fileExists(atPath: marker.path) { + return directory + } + directory = directory.deletingLastPathComponent() + } + throw EntitlementsParityError.repoRootNotFound + } + + private enum EntitlementsParityError: Error { + case repoRootNotFound + } +}