diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 106256f6..7f54c4e1 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -182,9 +182,9 @@ BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE8B2501AEB1007EE018 /* Keychain.swift */; }; BF66EE942501AEBC007EE018 /* ALTAppPermissions.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */; settings = {ATTRIBUTES = (Public, ); }; }; BF66EE952501AEBC007EE018 /* ALTSourceUserInfoKey.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; - BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */; }; + BF66EE962501AEBC007EE018 /* ALTPatreonBenefitID.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE902501AEBC007EE018 /* ALTPatreonBenefitID.m */; }; BF66EE972501AEBC007EE018 /* ALTAppPermissions.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */; }; - BF66EE982501AEBC007EE018 /* ALTPatreonBenefitType.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BF66EE982501AEBC007EE018 /* ALTPatreonBenefitID.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE922501AEBC007EE018 /* ALTPatreonBenefitID.h */; settings = {ATTRIBUTES = (Public, ); }; }; BF66EE992501AEBC007EE018 /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */; }; BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */; }; BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE9C2501AEC1007EE018 /* Fetchable.swift */; }; @@ -417,6 +417,8 @@ D5A299872AAB9E4E00A3988D /* ProcessError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1B2AA284ED00EF863D /* ProcessError.swift */; }; D5A299882AAB9E4E00A3988D /* JITError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1D2E32AA50EB60066CACC /* JITError.swift */; }; D5A299892AAB9E5900A3988D /* AppProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B7D2AA9226C00F61259 /* AppProcess.swift */; }; + D5A645232AF5B5C50047D980 /* PatreonAPI+Responses.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A645222AF5B5C50047D980 /* PatreonAPI+Responses.swift */; }; + D5A645252AF5BC7F0047D980 /* UserAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A645242AF5BC7F0047D980 /* UserAccount.swift */; }; D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */; }; D5B6F6A92AD75D01007EED5A /* ProcessInfo+Previews.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */; }; D5B6F6AB2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6F6AA2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift */; }; @@ -861,9 +863,9 @@ BF66EE8B2501AEB1007EE018 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTAppPermissions.h; sourceTree = ""; }; BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTSourceUserInfoKey.h; sourceTree = ""; }; - BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = ""; }; + BF66EE902501AEBC007EE018 /* ALTPatreonBenefitID.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitID.m; sourceTree = ""; }; BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermissions.m; sourceTree = ""; }; - BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = ""; }; + BF66EE922501AEBC007EE018 /* ALTPatreonBenefitID.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitID.h; sourceTree = ""; }; BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = ""; }; BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = ""; }; BF66EE9C2501AEC1007EE018 /* Fetchable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = ""; }; @@ -1085,6 +1087,8 @@ D5A1D2E82AA512940066CACC /* RemoteServiceDiscoveryTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteServiceDiscoveryTunnel.swift; sourceTree = ""; }; D5A1D2EA2AA513410066CACC /* URL+Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Tools.swift"; sourceTree = ""; }; D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedAPIs.swift; sourceTree = ""; }; + D5A645222AF5B5C50047D980 /* PatreonAPI+Responses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PatreonAPI+Responses.swift"; sourceTree = ""; }; + D5A645242AF5BC7F0047D980 /* UserAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAccount.swift; sourceTree = ""; }; D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearAppCacheOperation.swift; sourceTree = ""; }; D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+Previews.swift"; sourceTree = ""; }; D5B6F6AA2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewAppScreenshotsViewController.swift; sourceTree = ""; }; @@ -1639,8 +1643,8 @@ D5893F812A141E4900E767CD /* KnownSource.swift */, BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */, BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */, - BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */, - BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */, + BF66EE922501AEBC007EE018 /* ALTPatreonBenefitID.h */, + BF66EE902501AEBC007EE018 /* ALTPatreonBenefitID.m */, BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */, BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */, ); @@ -1661,11 +1665,13 @@ BF66EE9F2501AEC5007EE018 /* Patreon */ = { isa = PBXGroup; children = ( + BF66EEA12501AEC5007EE018 /* PatreonAPI.swift */, + D5A645222AF5B5C50047D980 /* PatreonAPI+Responses.swift */, BF66EEA02501AEC5007EE018 /* Benefit.swift */, BF66EEA22501AEC5007EE018 /* Campaign.swift */, - BF66EEA12501AEC5007EE018 /* PatreonAPI.swift */, BF66EEA32501AEC5007EE018 /* Patron.swift */, BF66EEA42501AEC5007EE018 /* Tier.swift */, + D5A645242AF5BC7F0047D980 /* UserAccount.swift */, ); path = Patreon; sourceTree = ""; @@ -2390,7 +2396,7 @@ files = ( BF66EE822501AE50007EE018 /* AltStoreCore.h in Headers */, BF66EE952501AEBC007EE018 /* ALTSourceUserInfoKey.h in Headers */, - BF66EE982501AEBC007EE018 /* ALTPatreonBenefitType.h in Headers */, + BF66EE982501AEBC007EE018 /* ALTPatreonBenefitID.h in Headers */, BFAECC5F2501B0BF00528F27 /* ALTConstants.h in Headers */, BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */, BF66EE942501AEBC007EE018 /* ALTAppPermissions.h in Headers */, @@ -3075,6 +3081,7 @@ BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */, BF66EEA52501AEC5007EE018 /* Benefit.swift in Sources */, BF66EED22501AECA007EE018 /* AltStore4ToAltStore5.xcmappingmodel in Sources */, + D5A645252AF5BC7F0047D980 /* UserAccount.swift in Sources */, D5189C022A01BC6800F44625 /* UserInfoValue.swift in Sources */, BFAECC5B2501B0A400528F27 /* Bundle+AltStore.swift in Sources */, BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */, @@ -3114,7 +3121,7 @@ D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */, D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */, BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */, - BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */, + BF66EE962501AEBC007EE018 /* ALTPatreonBenefitID.m in Sources */, BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */, D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */, BF66EEE92501AED0007EE018 /* JSONDecoder+Properties.swift in Sources */, @@ -3147,6 +3154,7 @@ BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */, 0EE7FDC92BE8D07400D1E390 /* NSError+AltStore.swift in Sources */, D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */, + D5A645232AF5B5C50047D980 /* PatreonAPI+Responses.swift in Sources */, D5185B822AE1E71D00646E33 /* Source13To14MigrationPolicy.swift in Sources */, BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */, BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */, diff --git a/AltStoreCore/AltStoreCore.h b/AltStoreCore/AltStoreCore.h index c3a5a398..1b14de51 100644 --- a/AltStoreCore/AltStoreCore.h +++ b/AltStoreCore/AltStoreCore.h @@ -18,7 +18,7 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[]; #import #import -#import +#import // Shared #import diff --git a/AltStoreCore/Model/ManagedPatron.swift b/AltStoreCore/Model/ManagedPatron.swift index 1e628b40..c5d67ac9 100644 --- a/AltStoreCore/Model/ManagedPatron.swift +++ b/AltStoreCore/Model/ManagedPatron.swift @@ -19,7 +19,7 @@ public class ManagedPatron: NSManagedObject, Fetchable super.init(entity: entity, insertInto: context) } - public init?(patron: Patron, context: NSManagedObjectContext) + public init?(patron: PatreonAPI.Patron, context: NSManagedObjectContext) { // Only cache Patrons with non-nil names. guard let name = patron.name else { return nil } diff --git a/AltStoreCore/Model/PatreonAccount.swift b/AltStoreCore/Model/PatreonAccount.swift index 3b2a32b2..94707a5d 100644 --- a/AltStoreCore/Model/PatreonAccount.swift +++ b/AltStoreCore/Model/PatreonAccount.swift @@ -8,27 +8,6 @@ import CoreData -extension PatreonAPI -{ - struct AccountResponse: Decodable - { - struct Data: Decodable - { - struct Attributes: Decodable - { - var first_name: String? - var full_name: String - } - - var id: String - var attributes: Attributes - } - - var data: Data - var included: [PatronResponse]? - } -} - @objc(PatreonAccount) public class PatreonAccount: NSManagedObject, Fetchable { @@ -44,13 +23,13 @@ public class PatreonAccount: NSManagedObject, Fetchable super.init(entity: entity, insertInto: context) } - init(response: PatreonAPI.AccountResponse, context: NSManagedObjectContext) + init(account: PatreonAPI.UserAccount, context: NSManagedObjectContext) { super.init(entity: PatreonAccount.entity(), insertInto: context) - self.identifier = response.data.id - self.name = response.data.attributes.full_name - self.firstName = response.data.attributes.first_name + self.identifier = account.identifier + self.name = account.name + self.firstName = account.firstName // if let patronResponse = response.included?.first // { diff --git a/AltStoreCore/Patreon/Benefit.swift b/AltStoreCore/Patreon/Benefit.swift index 63a29e9a..c1556b24 100644 --- a/AltStoreCore/Patreon/Benefit.swift +++ b/AltStoreCore/Patreon/Benefit.swift @@ -10,18 +10,25 @@ import Foundation extension PatreonAPI { - struct BenefitResponse: Decodable + typealias BenefitResponse = DataResponse + + struct BenefitAttributes: Decodable { - var id: String + var title: String } } -public struct Benefit: Hashable +extension PatreonAPI { - public var type: ALTPatreonBenefitType - - init(response: PatreonAPI.BenefitResponse) + public struct Benefit: Hashable { - self.type = ALTPatreonBenefitType(response.id) + public var name: String + public var identifier: ALTPatreonBenefitID + + internal init(response: BenefitResponse) + { + self.name = response.attributes.title + self.identifier = ALTPatreonBenefitID(response.id) + } } } diff --git a/AltStoreCore/Patreon/Campaign.swift b/AltStoreCore/Patreon/Campaign.swift index 00153eb9..68a1ea6d 100644 --- a/AltStoreCore/Patreon/Campaign.swift +++ b/AltStoreCore/Patreon/Campaign.swift @@ -10,18 +10,25 @@ import Foundation extension PatreonAPI { - struct CampaignResponse: Decodable + typealias CampaignResponse = DataResponse + + struct CampaignAttributes: Decodable { - var id: String + var url: URL } } -public struct Campaign +extension PatreonAPI { - public var identifier: String - - init(response: PatreonAPI.CampaignResponse) + public struct Campaign { - self.identifier = response.id + public var identifier: String + public var url: URL + + internal init(response: PatreonAPI.CampaignResponse) + { + self.identifier = response.id + self.url = response.attributes.url + } } } diff --git a/AltStoreCore/Patreon/PatreonAPI+Responses.swift b/AltStoreCore/Patreon/PatreonAPI+Responses.swift new file mode 100644 index 00000000..693d5e10 --- /dev/null +++ b/AltStoreCore/Patreon/PatreonAPI+Responses.swift @@ -0,0 +1,161 @@ +// +// PatreonAPI+Responses.swift +// AltStoreCore +// +// Created by Riley Testut on 11/3/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +protocol ResponseData: Decodable +{ +} + +// Allows us to use Arrays with Response<> despite them not conforming to `ItemResponse` +extension Array: ResponseData where Element: ItemResponse +{ +} + +protocol ItemResponse: ResponseData +{ + var id: String { get } + var type: String { get } +} + +extension PatreonAPI +{ + struct Response: Decodable + { + var data: Data + + var included: IncludedResponses? + var links: [String: URL]? + } + + struct AnyItemResponse: ItemResponse + { + var id: String + var type: String + } + + struct DataResponse: ItemResponse + { + var id: String + var type: String + + var attributes: Attributes + var relationships: Relationships? + } + + // `Never` only conforms to Decodable from iOS 17 onwards, + // so use our own "Empty" type for DataResponses without relationships. + struct AnyRelationships: Decodable + { + } + + struct IncludedResponses: Decodable + { + var items: [IncludedItem] + + var campaigns: [String: CampaignResponse] + var patrons: [String: PatronResponse] + var tiers: [String: TierResponse] + var benefits: [String: BenefitResponse] + + init(from decoder: Decoder) throws + { + let container = try decoder.singleValueContainer() + self.items = try container.decode([IncludedItem].self) + + var campaignsByID = [String: PatreonAPI.CampaignResponse]() + var patronsByID = [String: PatreonAPI.PatronResponse]() + var tiersByID = [String: PatreonAPI.TierResponse]() + var benefitsByID = [String: PatreonAPI.BenefitResponse]() + + for response in self.items + { + switch response + { + case .campaign(let response): campaignsByID[response.id] = response + case .patron(let response): patronsByID[response.id] = response + case .tier(let response): tiersByID[response.id] = response + case .benefit(let response): benefitsByID[response.id] = response + case .unknown: break // Ignore + } + } + + self.campaigns = campaignsByID + self.patrons = patronsByID + self.tiers = tiersByID + self.benefits = benefitsByID + } + } + + enum IncludedItem: ItemResponse + { + case tier(TierResponse) + case benefit(BenefitResponse) + case patron(PatronResponse) + case campaign(CampaignResponse) + case unknown(AnyItemResponse) + + var id: String { + switch self + { + case .tier(let response): return response.id + case .benefit(let response): return response.id + case .patron(let response): return response.id + case .campaign(let response): return response.id + case .unknown(let response): return response.id + } + } + + var type: String { + switch self + { + case .tier(let response): return response.type + case .benefit(let response): return response.type + case .patron(let response): return response.type + case .campaign(let response): return response.type + case .unknown(let response): return response.type + } + } + + private enum CodingKeys: String, CodingKey + { + case type + } + + init(from decoder: Decoder) throws + { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(String.self, forKey: .type) + switch type + { + case "tier": + let response = try TierResponse(from: decoder) + self = .tier(response) + + case "benefit": + let response = try BenefitResponse(from: decoder) + self = .benefit(response) + + case "member": + let response = try PatronResponse(from: decoder) + self = .patron(response) + + case "campaign": + let response = try CampaignResponse(from: decoder) + self = .campaign(response) + + default: + Logger.main.error("Unrecognized PatreonAPI response type: \(type, privacy: .public).") + + let response = try AnyItemResponse(from: decoder) + self = .unknown(response) + } + } + } +} diff --git a/AltStoreCore/Patreon/PatreonAPI.swift b/AltStoreCore/Patreon/PatreonAPI.swift index 2d19275c..59fb774e 100644 --- a/AltStoreCore/Patreon/PatreonAPI.swift +++ b/AltStoreCore/Patreon/PatreonAPI.swift @@ -32,61 +32,20 @@ enum PatreonAPIErrorCode: Int, ALTErrorEnum, CaseIterable } } -typealias PatreonAPIError = PatreonAPIErrorCode.Error -enum PatreonAPIErrorCode: Int, ALTErrorEnum, CaseIterable -{ - case unknown - case notAuthenticated - case invalidAccessToken - - var errorFailureReason: String { - switch self - { - case .unknown: return NSLocalizedString("An unknown error occurred.", comment: "") - case .notAuthenticated: return NSLocalizedString("No connected Patreon account.", comment: "") - case .invalidAccessToken: return NSLocalizedString("Invalid access token.", comment: "") - } - } -} extension PatreonAPI { + static let altstoreCampaignID = "2863968" + + typealias FetchAccountResponse = Response + typealias FriendZonePatronsResponse = Response<[PatronResponse]> + enum AuthorizationType { case none case user case creator } - - enum AnyResponse: Decodable - { - case tier(TierResponse) - case benefit(BenefitResponse) - - enum CodingKeys: String, CodingKey - { - case type - } - - init(from decoder: Decoder) throws - { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let type = try container.decode(String.self, forKey: .type) - switch type - { - case "tier": - let tier = try TierResponse(from: decoder) - self = .tier(tier) - - case "benefit": - let benefit = try BenefitResponse(from: decoder) - self = .benefit(benefit) - - default: throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unrecognized Patreon response type.") - } - } - } } public class PatreonAPI: NSObject @@ -155,14 +114,17 @@ public extension PatreonAPI func fetchAccount(completion: @escaping (Result) -> Void) { var components = URLComponents(string: "/api/oauth2/v2/identity")! - components.queryItems = [URLQueryItem(name: "include", value: "memberships"), + components.queryItems = [URLQueryItem(name: "include", value: "memberships.campaign.tiers,memberships.currently_entitled_tiers.benefits"), URLQueryItem(name: "fields[user]", value: "first_name,full_name"), URLQueryItem(name: "fields[member]", value: "full_name,patron_status")] + URLQueryItem(name: "fields[tier]", value: "title"), + URLQueryItem(name: "fields[benefit]", value: "title"), + URLQueryItem(name: "fields[campaign]", value: "url"), let requestURL = components.url(relativeTo: self.baseURL)! let request = URLRequest(url: requestURL) - self.send(request, authorizationType: .user) { (result: Result) in + self.send(request, authorizationType: .user) { (result: Result) in switch result { case .failure(~PatreonAPIErrorCode.notAuthenticated): @@ -170,10 +132,15 @@ public extension PatreonAPI completion(.failure(PatreonAPIError(.notAuthenticated))) } - case .failure(let error): completion(.failure(error)) + case .failure(let error as NSError): + Logger.main.error("Failed to fetch Patreon account. \(error.localizedDebugDescription ?? error.localizedDescription, privacy: .public)") + completion(.failure(error)) + case .success(let response): + let account = PatreonAPI.UserAccount(response: response.data, including: response.included) + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in - let account = PatreonAccount(response: response, context: context) + let account = PatreonAccount(account: account, context: context) Keychain.shared.patreonAccountID = account.identifier completion(.success(account)) } @@ -183,57 +150,34 @@ public extension PatreonAPI func fetchPatrons(completion: @escaping (Result<[Patron], Swift.Error>) -> Void) { - var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(campaignID)/members")! + var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(PatreonAPI.altstoreCampaignID)/members")! components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"), URLQueryItem(name: "fields[tier]", value: "title"), + URLQueryItem(name: "fields[benefit]", value: "title"), URLQueryItem(name: "fields[member]", value: "full_name,patron_status"), URLQueryItem(name: "page[size]", value: "1000")] let requestURL = components.url(relativeTo: self.baseURL)! - struct Response: Decodable - { - var data: [PatronResponse] - var included: [AnyResponse] - var links: [String: URL]? - } - var allPatrons = [Patron]() func fetchPatrons(url: URL) { let request = URLRequest(url: url) - self.send(request, authorizationType: .creator) { (result: Result) in + self.send(request, authorizationType: .creator) { (result: Result) in switch result { case .failure(let error): completion(.failure(error)) - case .success(let response): - let tiers = response.included.compactMap { (response) -> Tier? in - switch response - { - case .tier(let tierResponse): return Tier(response: tierResponse) - case .benefit: return nil - } - } - - let tiersByIdentifier = Dictionary(tiers.map { ($0.identifier, $0) }, uniquingKeysWith: { (a, b) in return a }) - - let patrons = response.data.map { (response) -> Patron in - let patron = Patron(response: response) - - for tierID in response.relationships?.currently_entitled_tiers.data ?? [] - { - guard let tier = tiersByIdentifier[tierID.id] else { continue } - patron.benefits.formUnion(tier.benefits) - } - + case .success(let patronsResponse): + let patrons = patronsResponse.data.map { (response) -> Patron in + let patron = Patron(response: response, including: patronsResponse.included) return patron - }.filter { $0.benefits.contains(where: { $0.type == .credits }) } + }.filter { $0.benefits.contains(where: { $0.identifier == .credits }) } allPatrons.append(contentsOf: patrons) - if let nextURL = response.links?["next"] + if let nextURL = patronsResponse.links?["next"] { fetchPatrons(url: nextURL) } diff --git a/AltStoreCore/Patreon/Patron.swift b/AltStoreCore/Patreon/Patron.swift index ef55bab4..04a76e43 100644 --- a/AltStoreCore/Patreon/Patron.swift +++ b/AltStoreCore/Patreon/Patron.swift @@ -10,38 +10,22 @@ import Foundation extension PatreonAPI { - struct PatronResponse: Decodable + typealias PatronResponse = DataResponse + + struct PatronAttributes: Decodable { - struct Attributes: Decodable - { - var full_name: String? - var patron_status: String? - } - - struct Relationships: Decodable - { - struct Tiers: Decodable - { - struct TierID: Decodable - { - var id: String - var type: String - } - - var data: [TierID] - } - - var currently_entitled_tiers: Tiers - } - - var id: String - var attributes: Attributes - - var relationships: Relationships? + var full_name: String? + var patron_status: String? + } + + struct PatronRelationships: Decodable + { + var campaign: Response? + var currently_entitled_tiers: Response<[AnyItemResponse]>? } } -extension Patron +extension PatreonAPI { public enum Status: String, Decodable { @@ -50,29 +34,46 @@ extension Patron case former = "former_patron" case unknown = "unknown" } -} - -public class Patron -{ - public var name: String? - public var identifier: String - public var status: Status - - public var benefits: Set = [] - - init(response: PatreonAPI.PatronResponse) + // Roughly equivalent to AltStoreCore.Pledge + public class Patron { - self.name = response.attributes.full_name - self.identifier = response.id + public var name: String? + public var identifier: String + public var status: Status - if let status = response.attributes.patron_status + // Relationships + public var campaign: Campaign? + public var tiers: Set = [] + public var benefits: Set = [] + + internal init(response: PatronResponse, including included: IncludedResponses?) { - self.status = Status(rawValue: status) ?? .unknown - } - else - { - self.status = .unknown + self.name = response.attributes.full_name + self.identifier = response.id + + if let status = response.attributes.patron_status + { + self.status = Status(rawValue: status) ?? .unknown + } + else + { + self.status = .unknown + } + + guard let included, let relationships = response.relationships else { return } + + if let campaignID = relationships.campaign?.data.id, let response = included.campaigns[campaignID] + { + let campaign = Campaign(response: response) + self.campaign = campaign + } + + let tiers = (relationships.currently_entitled_tiers?.data ?? []).compactMap { included.tiers[$0.id] }.map { Tier(response: $0, including: included) } + self.tiers = Set(tiers) + + let benefits = tiers.flatMap { $0.benefits } + self.benefits = Set(benefits) } } } diff --git a/AltStoreCore/Patreon/Tier.swift b/AltStoreCore/Patreon/Tier.swift index 0bf0dc88..f26c670a 100644 --- a/AltStoreCore/Patreon/Tier.swift +++ b/AltStoreCore/Patreon/Tier.swift @@ -10,41 +10,38 @@ import Foundation extension PatreonAPI { - struct TierResponse: Decodable + typealias TierResponse = DataResponse + + struct TierAttributes: Decodable { - struct Attributes: Decodable - { - var title: String - } - - struct Relationships: Decodable - { - struct Benefits: Decodable - { - var data: [BenefitResponse] - } - - var benefits: Benefits - } - - var id: String - var attributes: Attributes - - var relationships: Relationships + var title: String + } + + struct TierRelationships: Decodable + { + var benefits: Response<[AnyItemResponse]>? } } -public struct Tier +extension PatreonAPI { - public var name: String - public var identifier: String - - public var benefits: [Benefit] = [] - - init(response: PatreonAPI.TierResponse) + public struct Tier: Hashable { - self.name = response.attributes.title - self.identifier = response.id - self.benefits = response.relationships.benefits.data.map(Benefit.init(response:)) + public var name: String + public var identifier: String + + // Relationships + public var benefits: [Benefit] = [] + + internal init(response: TierResponse, including included: IncludedResponses?) + { + self.name = response.attributes.title + self.identifier = response.id + + guard let included, let benefitIDs = response.relationships?.benefits?.data.map(\.id) else { return } + + let benefits = benefitIDs.compactMap { included.benefits[$0] }.map(Benefit.init(response:)) + self.benefits = benefits + } } } diff --git a/AltStoreCore/Patreon/UserAccount.swift b/AltStoreCore/Patreon/UserAccount.swift new file mode 100644 index 00000000..83e52e47 --- /dev/null +++ b/AltStoreCore/Patreon/UserAccount.swift @@ -0,0 +1,49 @@ +// +// Account.swift +// AltStoreCore +// +// Created by Riley Testut on 11/3/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +extension PatreonAPI +{ + typealias UserAccountResponse = DataResponse + + struct UserAccountAttributes: Decodable + { + var first_name: String? + var full_name: String + } +} + +extension PatreonAPI +{ + public struct UserAccount + { + var name: String + var firstName: String? + var identifier: String + + // Relationships + var pledges: [Patron]? + + init(response: UserAccountResponse, including included: IncludedResponses?) + { + self.identifier = response.id + self.name = response.attributes.full_name + self.firstName = response.attributes.first_name + + guard let included else { return } + + let patrons = included.patrons.values.compactMap { response -> Patron? in + let patron = Patron(response: response, including: included) + return patron + } + + self.pledges = patrons + } + } +} diff --git a/AltStoreCore/Types/ALTPatreonBenefitID.h b/AltStoreCore/Types/ALTPatreonBenefitID.h new file mode 100644 index 00000000..e5396489 --- /dev/null +++ b/AltStoreCore/Types/ALTPatreonBenefitID.h @@ -0,0 +1,13 @@ +// +// ALTPatreonBenefitType.h +// AltStore +// +// Created by Riley Testut on 8/27/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +#import + +typedef NSString *ALTPatreonBenefitID NS_TYPED_EXTENSIBLE_ENUM; +extern ALTPatreonBenefitID const ALTPatreonBenefitIDBetaAccess; +extern ALTPatreonBenefitID const ALTPatreonBenefitIDCredits; diff --git a/AltStoreCore/Types/ALTPatreonBenefitID.m b/AltStoreCore/Types/ALTPatreonBenefitID.m new file mode 100644 index 00000000..60e87da1 --- /dev/null +++ b/AltStoreCore/Types/ALTPatreonBenefitID.m @@ -0,0 +1,12 @@ +// +// ALTPatreonBenefitType.m +// AltStore +// +// Created by Riley Testut on 8/27/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +#import "ALTPatreonBenefitID.h" + +ALTPatreonBenefitID const ALTPatreonBenefitIDBetaAccess = @"1186336"; +ALTPatreonBenefitID const ALTPatreonBenefitIDCredits = @"1186340"; diff --git a/AltStoreCore/Types/ALTPatreonBenefitType.h b/AltStoreCore/Types/ALTPatreonBenefitType.h deleted file mode 100644 index 83ad9b66..00000000 --- a/AltStoreCore/Types/ALTPatreonBenefitType.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// ALTPatreonBenefitType.h -// AltStore -// -// Created by Riley Testut on 8/27/19. -// Copyright © 2019 Riley Testut. All rights reserved. -// - -#import - -typedef NSString *ALTPatreonBenefitType NS_TYPED_EXTENSIBLE_ENUM; -extern ALTPatreonBenefitType const ALTPatreonBenefitTypeBetaAccess; -extern ALTPatreonBenefitType const ALTPatreonBenefitTypeCredits; diff --git a/AltStoreCore/Types/ALTPatreonBenefitType.m b/AltStoreCore/Types/ALTPatreonBenefitType.m deleted file mode 100644 index 7f6bf5af..00000000 --- a/AltStoreCore/Types/ALTPatreonBenefitType.m +++ /dev/null @@ -1,12 +0,0 @@ -// -// ALTPatreonBenefitType.m -// AltStore -// -// Created by Riley Testut on 8/27/19. -// Copyright © 2019 Riley Testut. All rights reserved. -// - -#import "ALTPatreonBenefitType.h" - -ALTPatreonBenefitType const ALTPatreonBenefitTypeBetaAccess = @"1186336"; -ALTPatreonBenefitType const ALTPatreonBenefitTypeCredits = @"1186340";