From 887e958dd1513d37dc715189e4070435b7f844fd Mon Sep 17 00:00:00 2001 From: Cesar Andres Pereira Date: Mon, 28 Sep 2026 15:30:11 -0500 Subject: [PATCH 1/2] Allow one app group on free teams Resolves the FIXME in DeveloperServicesCapability: free teams can register and assign app groups through the Xcode DeveloperServices API, so App Groups are no longer filtered out for them, and DeveloperServicesAddAppOperation assigns at most one group per app on a free team. --- .../App IDs/DeveloperServicesAddAppOperation.swift | 3 ++- .../App IDs/Entitlements/DeveloperServicesCapability.swift | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift b/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift index d726e91e..ac872719 100644 --- a/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift +++ b/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift @@ -206,7 +206,8 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation { if var entitlementsArray = try? entitlements.entitlements(), let groupsIdx = entitlementsArray.firstIndex(where: { $0 is AppGroupEntitlement }), let groupsEntitlement = entitlementsArray[groupsIdx] as? AppGroupEntitlement { - let groups = groupsEntitlement.rawValue + // Free teams get at most one app group per app. + let groups = isFreeTeam ? Array(groupsEntitlement.rawValue.prefix(1)) : groupsEntitlement.rawValue if let operation = DeveloperServicesAssignAppGroupsOperation( context: self.context, diff --git a/Sources/XKit/DeveloperServices/App IDs/Entitlements/DeveloperServicesCapability.swift b/Sources/XKit/DeveloperServices/App IDs/Entitlements/DeveloperServicesCapability.swift index 2dca4637..2e922058 100644 --- a/Sources/XKit/DeveloperServices/App IDs/Entitlements/DeveloperServicesCapability.swift +++ b/Sources/XKit/DeveloperServices/App IDs/Entitlements/DeveloperServicesCapability.swift @@ -73,9 +73,9 @@ extension DataProtectionEntitlement: EntitlementWithCapability { extension AppGroupEntitlement: EntitlementWithCapability { var capability: DeveloperServicesCapability { - // FIXME: Enable app groups on free accounts - // but we need to always assign only one group - DeveloperServicesCapability(.appGroups, isFree: false) + // Free teams can use App Groups too; DeveloperServicesAddAppOperation + // assigns them at most one. + DeveloperServicesCapability(.appGroups, isFree: true) } } From f340cd2778e10ab86b0824329037eb322a727bda Mon Sep 17 00:00:00 2001 From: Cesar Andres Pereira Bernal Date: Wed, 21 Oct 2026 09:59:11 -0500 Subject: [PATCH 2/2] Throw on multiple app groups for free teams Co-Authored-By: Claude Opus 5.5 --- .../DeveloperServicesAddAppOperation.swift | 18 ++++++++++++++++-- .../DeveloperServicesCapability.swift | 2 -- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift b/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift index ac872719..23aca94b 100644 --- a/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift +++ b/Sources/XKit/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift @@ -13,6 +13,7 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation { public enum Error: LocalizedError { case invalidApp(URL) + case tooManyAppGroups(bundleID: String, count: Int) public var errorDescription: String? { switch self { @@ -24,6 +25,16 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation { ), $0 ) } + case .tooManyAppGroups(let bundleID, let count): + return bundleID.withCString { + String.localizedStringWithFormat( + NSLocalizedString( + "add_app_operation.error.too_many_app_groups", + value: "%s requests %d app groups, but free teams can only use one app group per app", + comment: "" + ), $0, count + ) + } } } } @@ -180,6 +191,10 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation { // re-assign rather than using updateEntitlements since the latter will // retain unrecognized entitlements let filtered = try entitlements.entitlements().filter { $0.anyCapability?.isFree != false } + if let groups = filtered.lazy.compactMap({ $0 as? AppGroupEntitlement }).first?.rawValue, + groups.count > 1 { + throw Error.tooManyAppGroups(bundleID: bundleID, count: groups.count) + } entitlements = try Entitlements(entitlements: filtered) } } else { @@ -206,8 +221,7 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation { if var entitlementsArray = try? entitlements.entitlements(), let groupsIdx = entitlementsArray.firstIndex(where: { $0 is AppGroupEntitlement }), let groupsEntitlement = entitlementsArray[groupsIdx] as? AppGroupEntitlement { - // Free teams get at most one app group per app. - let groups = isFreeTeam ? Array(groupsEntitlement.rawValue.prefix(1)) : groupsEntitlement.rawValue + let groups = groupsEntitlement.rawValue if let operation = DeveloperServicesAssignAppGroupsOperation( context: self.context, diff --git a/Sources/XKit/DeveloperServices/App IDs/Entitlements/DeveloperServicesCapability.swift b/Sources/XKit/DeveloperServices/App IDs/Entitlements/DeveloperServicesCapability.swift index 2e922058..9af5eb65 100644 --- a/Sources/XKit/DeveloperServices/App IDs/Entitlements/DeveloperServicesCapability.swift +++ b/Sources/XKit/DeveloperServices/App IDs/Entitlements/DeveloperServicesCapability.swift @@ -73,8 +73,6 @@ extension DataProtectionEntitlement: EntitlementWithCapability { extension AppGroupEntitlement: EntitlementWithCapability { var capability: DeveloperServicesCapability { - // Free teams can use App Groups too; DeveloperServicesAddAppOperation - // assigns them at most one. DeveloperServicesCapability(.appGroups, isFree: true) } }