mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
[AltStoreCore] Refactors PatreonAPI to reduce duplicate logic
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTAppPermissions.h; sourceTree = "<group>"; };
|
||||
BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTSourceUserInfoKey.h; sourceTree = "<group>"; };
|
||||
BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = "<group>"; };
|
||||
BF66EE902501AEBC007EE018 /* ALTPatreonBenefitID.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitID.m; sourceTree = "<group>"; };
|
||||
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermissions.m; sourceTree = "<group>"; };
|
||||
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = "<group>"; };
|
||||
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitID.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitID.h; sourceTree = "<group>"; };
|
||||
BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = "<group>"; };
|
||||
BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = "<group>"; };
|
||||
BF66EE9C2501AEC1007EE018 /* Fetchable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = "<group>"; };
|
||||
@@ -1085,6 +1087,8 @@
|
||||
D5A1D2E82AA512940066CACC /* RemoteServiceDiscoveryTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteServiceDiscoveryTunnel.swift; sourceTree = "<group>"; };
|
||||
D5A1D2EA2AA513410066CACC /* URL+Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Tools.swift"; sourceTree = "<group>"; };
|
||||
D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedAPIs.swift; sourceTree = "<group>"; };
|
||||
D5A645222AF5B5C50047D980 /* PatreonAPI+Responses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PatreonAPI+Responses.swift"; sourceTree = "<group>"; };
|
||||
D5A645242AF5BC7F0047D980 /* UserAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAccount.swift; sourceTree = "<group>"; };
|
||||
D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearAppCacheOperation.swift; sourceTree = "<group>"; };
|
||||
D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+Previews.swift"; sourceTree = "<group>"; };
|
||||
D5B6F6AA2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewAppScreenshotsViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -18,7 +18,7 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
|
||||
|
||||
#import <AltStoreCore/ALTAppPermissions.h>
|
||||
#import <AltStoreCore/ALTSourceUserInfoKey.h>
|
||||
#import <AltStoreCore/ALTPatreonBenefitType.h>
|
||||
#import <AltStoreCore/ALTPatreonBenefitID.h>
|
||||
|
||||
// Shared
|
||||
#import <AltStoreCore/ALTConstants.h>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
// {
|
||||
|
||||
@@ -10,18 +10,25 @@ import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct BenefitResponse: Decodable
|
||||
typealias BenefitResponse = DataResponse<BenefitAttributes, AnyRelationships>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,25 @@ import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct CampaignResponse: Decodable
|
||||
typealias CampaignResponse = DataResponse<CampaignAttributes, AnyRelationships>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
161
AltStoreCore/Patreon/PatreonAPI+Responses.swift
Normal file
161
AltStoreCore/Patreon/PatreonAPI+Responses.swift
Normal file
@@ -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<Data: ResponseData>: Decodable
|
||||
{
|
||||
var data: Data
|
||||
|
||||
var included: IncludedResponses?
|
||||
var links: [String: URL]?
|
||||
}
|
||||
|
||||
struct AnyItemResponse: ItemResponse
|
||||
{
|
||||
var id: String
|
||||
var type: String
|
||||
}
|
||||
|
||||
struct DataResponse<Attributes: Decodable, Relationships: Decodable>: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<UserAccountResponse>
|
||||
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<PatreonAccount, Swift.Error>) -> 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<AccountResponse, Swift.Error>) in
|
||||
self.send(request, authorizationType: .user) { (result: Result<FetchAccountResponse, Swift.Error>) 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<Response, Swift.Error>) in
|
||||
self.send(request, authorizationType: .creator) { (result: Result<FriendZonePatronsResponse, Swift.Error>) 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)
|
||||
}
|
||||
|
||||
@@ -10,38 +10,22 @@ import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct PatronResponse: Decodable
|
||||
typealias PatronResponse = DataResponse<PatronAttributes, PatronRelationships>
|
||||
|
||||
struct PatronAttributes: Decodable
|
||||
{
|
||||
struct Attributes: Decodable
|
||||
{
|
||||
var full_name: String?
|
||||
var patron_status: String?
|
||||
}
|
||||
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?
|
||||
struct PatronRelationships: Decodable
|
||||
{
|
||||
var campaign: Response<AnyItemResponse>?
|
||||
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<Benefit> = []
|
||||
|
||||
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<Tier> = []
|
||||
public var benefits: Set<Benefit> = []
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,41 +10,38 @@ import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct TierResponse: Decodable
|
||||
typealias TierResponse = DataResponse<TierAttributes, TierRelationships>
|
||||
|
||||
struct TierAttributes: Decodable
|
||||
{
|
||||
struct Attributes: Decodable
|
||||
{
|
||||
var title: String
|
||||
}
|
||||
var title: String
|
||||
}
|
||||
|
||||
struct Relationships: Decodable
|
||||
{
|
||||
struct Benefits: Decodable
|
||||
{
|
||||
var data: [BenefitResponse]
|
||||
}
|
||||
|
||||
var benefits: Benefits
|
||||
}
|
||||
|
||||
var id: String
|
||||
var attributes: Attributes
|
||||
|
||||
var relationships: Relationships
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
AltStoreCore/Patreon/UserAccount.swift
Normal file
49
AltStoreCore/Patreon/UserAccount.swift
Normal file
@@ -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<UserAccountAttributes, AnyRelationships>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
13
AltStoreCore/Types/ALTPatreonBenefitID.h
Normal file
13
AltStoreCore/Types/ALTPatreonBenefitID.h
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// ALTPatreonBenefitType.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/27/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NSString *ALTPatreonBenefitID NS_TYPED_EXTENSIBLE_ENUM;
|
||||
extern ALTPatreonBenefitID const ALTPatreonBenefitIDBetaAccess;
|
||||
extern ALTPatreonBenefitID const ALTPatreonBenefitIDCredits;
|
||||
12
AltStoreCore/Types/ALTPatreonBenefitID.m
Normal file
12
AltStoreCore/Types/ALTPatreonBenefitID.m
Normal file
@@ -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";
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// ALTPatreonBenefitType.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/27/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NSString *ALTPatreonBenefitType NS_TYPED_EXTENSIBLE_ENUM;
|
||||
extern ALTPatreonBenefitType const ALTPatreonBenefitTypeBetaAccess;
|
||||
extern ALTPatreonBenefitType const ALTPatreonBenefitTypeCredits;
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user