diff options
| author | Christian Cleberg <[email protected]> | 2026-07-25 11:59:44 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-25 11:59:44 -0500 |
| commit | 030187ece649f3a6e28b298126884602aef16cf0 (patch) | |
| tree | 2e2506a7d1db92c8195b50294a5e121bda7cb352 | |
| parent | 7156c088a438526182c517fafa35d31990d44f18 (diff) | |
| download | domain-dig-030187ece649f3a6e28b298126884602aef16cf0.tar.gz domain-dig-030187ece649f3a6e28b298126884602aef16cf0.tar.bz2 domain-dig-030187ece649f3a6e28b298126884602aef16cf0.zip | |
chore: add DEBUG reveal of the CloudKit owner record IDchore/owner-record-id-reveal
Groundwork for the owner Pro+ allowlist. Adds OwnerAccess, which identifies the
app owner by their CloudKit user-record ID (a stable, opaque per-Apple-ID value
that cannot be guessed or spoofed), plus a DEBUG-only "Developer" row in
Settings → App Info that fetches and copies the current iCloud user's record ID
for the app's container.
OwnerAccess.ownerUserRecordID is empty, so isOwner() is inert until the real
value is filled in. A follow-up will hardcode the owner ID, wire isOwner() into
PurchaseService to grant .proPlus on a match, remove this reveal, and cut 5.0.1.
DEBUG-only UI; release builds are unaffected.
| -rw-r--r-- | DomainDig/AppInfoView.swift | 24 | ||||
| -rw-r--r-- | DomainDig/OwnerAccess.swift | 34 |
2 files changed, 58 insertions, 0 deletions
diff --git a/DomainDig/AppInfoView.swift b/DomainDig/AppInfoView.swift index 055fe46..f3a0765 100644 --- a/DomainDig/AppInfoView.swift +++ b/DomainDig/AppInfoView.swift @@ -8,6 +8,9 @@ struct AppInfoView: View { @Environment(\.openURL) private var openURL @State private var cloudSyncService = CloudSyncService.shared @State private var activeSheet: AppInfoSheet? + #if DEBUG + @State private var developerRecordName: String? + #endif var body: some View { Form { @@ -44,6 +47,27 @@ struct AppInfoView: View { Text(AppLinks.copyright) .frame(maxWidth: .infinity, alignment: .center) } + + #if DEBUG + Section { + HStack(alignment: .top, spacing: 8) { + Text(developerRecordName ?? "Fetching…") + .font(.system(.footnote, design: .monospaced)) + .textSelection(.enabled) + Spacer(minLength: 8) + if let developerRecordName { + AppCopyButton(value: developerRecordName, label: "Copy iCloud record ID") + } + } + } header: { + Text("Developer") + } footer: { + Text("Your CloudKit user-record ID for this app, used to configure the owner allowlist. DEBUG builds only.") + } + .task { + developerRecordName = await OwnerAccess.currentUserRecordName() ?? "Unavailable (sign into iCloud)" + } + #endif } .navigationTitle("App Info") .task { diff --git a/DomainDig/OwnerAccess.swift b/DomainDig/OwnerAccess.swift new file mode 100644 index 0000000..be39a6c --- /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 +/// that other users cannot guess or spoof — so a release build can grant the +/// owner Pro+ without a purchase. +/// +/// `ownerUserRecordID` is empty until configured; an empty value never matches, +/// so the allowlist is inert until the owner's real record ID is filled in. Read +/// your own value from the DEBUG "Developer" row in Settings → App Info. +enum OwnerAccess { + /// The owner's CloudKit user-record name. Empty = unconfigured (inert). + static let ownerUserRecordID = "" + + 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 + } +} |
