diff --git a/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupRequest.swift b/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupRequest.swift index 713afdb0..399ed8c6 100644 --- a/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupRequest.swift +++ b/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupRequest.swift @@ -8,6 +8,9 @@ import Foundation +/// Assigns a set of app groups to an App ID. The request replaces the App ID's groups with +/// `groupIDs`, so every group the app needs has to be in one request: assigning them one +/// request at a time leaves only the last one standing. public final class DeveloperServicesAssignAppGroupRequest: DeveloperServicesPlatformRequest { public struct Response: Decodable, Sendable {} @@ -16,14 +19,14 @@ public final class DeveloperServicesAssignAppGroupRequest: DeveloperServicesPlat public let platform: DeveloperServicesPlatform public let teamID: DeveloperServicesTeam.ID public let appIDID: String - public let groupID: DeveloperServicesAppGroup.ID + public let groupIDs: [DeveloperServicesAppGroup.ID] var subAction: String { return "assignApplicationGroupToAppId" } var subParameters: [String: Any] { return [ "teamId": teamID.rawValue, "appIdId": appIDID, - "applicationGroups": groupID.rawValue + "applicationGroups": groupIDs.map(\.rawValue) ] } @@ -31,12 +34,12 @@ public final class DeveloperServicesAssignAppGroupRequest: DeveloperServicesPlat platform: DeveloperServicesPlatform, teamID: DeveloperServicesTeam.ID, appIDID: String, - groupID: DeveloperServicesAppGroup.ID + groupIDs: [DeveloperServicesAppGroup.ID] ) { self.platform = platform self.teamID = teamID self.appIDID = appIDID - self.groupID = groupID + self.groupIDs = groupIDs } } diff --git a/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupsOperation.swift b/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupsOperation.swift index b13e1a5b..d10fa846 100644 --- a/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupsOperation.swift +++ b/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupsOperation.swift @@ -33,52 +33,60 @@ public struct DeveloperServicesAssignAppGroupsOperation: DeveloperServicesOperat self.client = DeveloperServicesClient(authData: authData) } + /// Finds the team's group for `groupID`, creating it when there is none. private func upsertAppGroup( _ groupID: DeveloperServicesAppGroup.GroupID, existingGroups: [String: DeveloperServicesAppGroup] - ) async throws -> DeveloperServicesAppGroup.GroupID { + ) async throws -> DeveloperServicesAppGroup { let sanitized = ProvisioningIdentifiers.sanitize(groupID: groupID) - let group: DeveloperServicesAppGroup if let existingGroup = existingGroups[sanitized] { - group = existingGroup - } else { - let groupID = ProvisioningIdentifiers.groupID(fromSanitized: sanitized, context: context) - let name = ProvisioningIdentifiers.groupName(fromSanitized: sanitized) - let request = DeveloperServicesAddAppGroupRequest( - platform: .iOS, - teamID: xcodeAuthData.teamID, - name: name, - groupID: groupID - ) - group = try await client.send(request) + return existingGroup } - - _ = try await client.send( - DeveloperServicesAssignAppGroupRequest( - platform: .iOS, - teamID: xcodeAuthData.teamID, - appIDID: appID.id, - groupID: group.id - ) + let groupID = ProvisioningIdentifiers.groupID(fromSanitized: sanitized, context: context) + let name = ProvisioningIdentifiers.groupName(fromSanitized: sanitized) + let request = DeveloperServicesAddAppGroupRequest( + platform: .iOS, + teamID: xcodeAuthData.teamID, + name: name, + groupID: groupID ) - - return group.groupID + return try await client.send(request) } + /// Creates any missing groups, then assigns all of them to the App ID in one request. + /// `assignApplicationGroupToAppId` replaces the App ID's groups with the ones in the request, + /// so assigning each group separately (as this once did, concurrently) left the App ID with + /// only one of them, and an app with two groups got a profile that iOS refused to install. + /// Returns the groups' IDs in the order they were given. public func perform() async throws -> [DeveloperServicesAppGroup.GroupID] { let existing = try await client.send(DeveloperServicesListAppGroupsRequest( platform: .iOS, teamID: xcodeAuthData.teamID )) let sanitized = existing.map { (ProvisioningIdentifiers.sanitize(groupID: $0.groupID), $0) } let dict = Dictionary(sanitized, uniquingKeysWith: { $1 }) - return try await withThrowingTaskGroup(of: DeveloperServicesAppGroup.GroupID.self) { group in - for groupID in groupIDs { + let groups = try await withThrowingTaskGroup(of: (Int, DeveloperServicesAppGroup).self) { group in + for (index, groupID) in groupIDs.enumerated() { group.addTask { - try await upsertAppGroup(groupID, existingGroups: dict) + (index, try await upsertAppGroup(groupID, existingGroups: dict)) } } return try await group.reduce(into: []) { $0.append($1) } } + .sorted { $0.0 < $1.0 } + .map(\.1) + + if !groups.isEmpty { + _ = try await client.send( + DeveloperServicesAssignAppGroupRequest( + platform: .iOS, + teamID: xcodeAuthData.teamID, + appIDID: appID.id, + groupIDs: groups.map(\.id) + ) + ) + } + + return groups.map(\.groupID) } }