From 963ce3fb34bd6c6bd72f1aa956ce602267b90e9c Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sat, 25 Jul 2026 12:14:58 -0500 Subject: feat: owner Pro+ allowlist via CloudKit, cut v5.0.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grants the app owner Pro+ without a purchase, keyed to their CloudKit user-record ID so it works on the release App Store build. - OwnerAccess holds the owner's CloudKit user-record ID (opaque, per-Apple-ID, scoped to the app's container; safe to publish — CloudKit verifies identity server-side, so it can't be presented by anyone else). - PurchaseService resolves the allowlist against CloudKit once per launch and, on a match, records a persisted owner grant so it applies instantly and offline thereafter. The grant only ever elevates the tier to .proPlus and defers to the existing #if DEBUG overrides, so real purchases and free/pro testing are unaffected. cachedTier / cachedEntitlement were refactored to fall back to the owner grant only when no debug override or stored purchase applies. - Supersedes the DEBUG record-ID reveal (PR #60): its only purpose was to read the owner's ID, which is now hardcoded, so the reveal is not shipped. Release cut: MARKETING_VERSION 5.0.0 -> 5.0.1, CURRENT_PROJECT_VERSION 45 -> 46, AppVersion.current -> 5.0.1, roadmap updated. App builds clean; unit suite 63/63. --- DomainDig/OwnerAccess.swift | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 DomainDig/OwnerAccess.swift (limited to 'DomainDig/OwnerAccess.swift') diff --git a/DomainDig/OwnerAccess.swift b/DomainDig/OwnerAccess.swift new file mode 100644 index 0000000..eca2bab --- /dev/null +++ b/DomainDig/OwnerAccess.swift @@ -0,0 +1,34 @@ +import CloudKit + +/// Owner-only entitlement support. The app owner is identified by their CloudKit +/// user-record ID — a stable, opaque per-Apple-ID value for this app's container. +/// `PurchaseService` grants the owner Pro+ when the signed-in iCloud user matches, +/// so the owner does not need a purchase. +/// +/// Publishing the record ID here is safe: it is not an Apple ID or any personal +/// identifier, it is scoped to the `iCloud.net.cleberg.DomainDig` container, and +/// CloudKit identity is verified server-side — another user cannot present it as +/// their own. An empty value makes the allowlist inert. +enum OwnerAccess { + static let ownerUserRecordID = "_1c35d6a25540b3ef00023cc0425ec373" + + static var isConfigured: Bool { !ownerUserRecordID.isEmpty } + + /// The current iCloud user's record name for this app's container, or nil if + /// it is unavailable (not signed into iCloud, restricted, or offline before + /// the first fetch). + static func currentUserRecordName() async -> String? { + do { + return try await CKContainer.default().userRecordID().recordName + } catch { + return nil + } + } + + /// True only when the allowlist is configured and the current iCloud user is + /// the owner. + static func isOwner() async -> Bool { + guard isConfigured else { return false } + return await currentUserRecordName() == ownerUserRecordID + } +} -- cgit v1.2.3