[AltStoreCore] Refactors PatreonAPI to reduce duplicate logic

This commit is contained in:
Riley Testut
2023-11-15 14:13:58 -06:00
committed by Magesh K
parent 6ba642335b
commit 99a3746e1a
15 changed files with 386 additions and 233 deletions

View File

@@ -182,9 +182,9 @@
BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE8B2501AEB1007EE018 /* Keychain.swift */; }; 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, ); }; }; 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, ); }; }; 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 */; }; 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 */; }; BF66EE992501AEBC007EE018 /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */; };
BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */; }; BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE9B2501AEC1007EE018 /* AppProtocol.swift */; };
BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EE9C2501AEC1007EE018 /* Fetchable.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 */; }; D5A299872AAB9E4E00A3988D /* ProcessError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1B2AA284ED00EF863D /* ProcessError.swift */; };
D5A299882AAB9E4E00A3988D /* JITError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1D2E32AA50EB60066CACC /* JITError.swift */; }; D5A299882AAB9E4E00A3988D /* JITError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1D2E32AA50EB60066CACC /* JITError.swift */; };
D5A299892AAB9E5900A3988D /* AppProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B7D2AA9226C00F61259 /* AppProcess.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 */; }; D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */; };
D5B6F6A92AD75D01007EED5A /* ProcessInfo+Previews.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; D5B6F6AA2AD76541007EED5A /* PreviewAppScreenshotsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewAppScreenshotsViewController.swift; sourceTree = "<group>"; };
@@ -1639,8 +1643,8 @@
D5893F812A141E4900E767CD /* KnownSource.swift */, D5893F812A141E4900E767CD /* KnownSource.swift */,
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */, BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */,
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */, BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */,
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitType.h */, BF66EE922501AEBC007EE018 /* ALTPatreonBenefitID.h */,
BF66EE902501AEBC007EE018 /* ALTPatreonBenefitType.m */, BF66EE902501AEBC007EE018 /* ALTPatreonBenefitID.m */,
BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */, BF66EE8F2501AEBC007EE018 /* ALTSourceUserInfoKey.h */,
BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */, BF66EE932501AEBC007EE018 /* ALTSourceUserInfoKey.m */,
); );
@@ -1661,11 +1665,13 @@
BF66EE9F2501AEC5007EE018 /* Patreon */ = { BF66EE9F2501AEC5007EE018 /* Patreon */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BF66EEA12501AEC5007EE018 /* PatreonAPI.swift */,
D5A645222AF5B5C50047D980 /* PatreonAPI+Responses.swift */,
BF66EEA02501AEC5007EE018 /* Benefit.swift */, BF66EEA02501AEC5007EE018 /* Benefit.swift */,
BF66EEA22501AEC5007EE018 /* Campaign.swift */, BF66EEA22501AEC5007EE018 /* Campaign.swift */,
BF66EEA12501AEC5007EE018 /* PatreonAPI.swift */,
BF66EEA32501AEC5007EE018 /* Patron.swift */, BF66EEA32501AEC5007EE018 /* Patron.swift */,
BF66EEA42501AEC5007EE018 /* Tier.swift */, BF66EEA42501AEC5007EE018 /* Tier.swift */,
D5A645242AF5BC7F0047D980 /* UserAccount.swift */,
); );
path = Patreon; path = Patreon;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -2390,7 +2396,7 @@
files = ( files = (
BF66EE822501AE50007EE018 /* AltStoreCore.h in Headers */, BF66EE822501AE50007EE018 /* AltStoreCore.h in Headers */,
BF66EE952501AEBC007EE018 /* ALTSourceUserInfoKey.h in Headers */, BF66EE952501AEBC007EE018 /* ALTSourceUserInfoKey.h in Headers */,
BF66EE982501AEBC007EE018 /* ALTPatreonBenefitType.h in Headers */, BF66EE982501AEBC007EE018 /* ALTPatreonBenefitID.h in Headers */,
BFAECC5F2501B0BF00528F27 /* ALTConstants.h in Headers */, BFAECC5F2501B0BF00528F27 /* ALTConstants.h in Headers */,
BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */, BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */,
BF66EE942501AEBC007EE018 /* ALTAppPermissions.h in Headers */, BF66EE942501AEBC007EE018 /* ALTAppPermissions.h in Headers */,
@@ -3075,6 +3081,7 @@
BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */, BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
BF66EEA52501AEC5007EE018 /* Benefit.swift in Sources */, BF66EEA52501AEC5007EE018 /* Benefit.swift in Sources */,
BF66EED22501AECA007EE018 /* AltStore4ToAltStore5.xcmappingmodel in Sources */, BF66EED22501AECA007EE018 /* AltStore4ToAltStore5.xcmappingmodel in Sources */,
D5A645252AF5BC7F0047D980 /* UserAccount.swift in Sources */,
D5189C022A01BC6800F44625 /* UserInfoValue.swift in Sources */, D5189C022A01BC6800F44625 /* UserInfoValue.swift in Sources */,
BFAECC5B2501B0A400528F27 /* Bundle+AltStore.swift in Sources */, BFAECC5B2501B0A400528F27 /* Bundle+AltStore.swift in Sources */,
BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */, BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */,
@@ -3114,7 +3121,7 @@
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */, D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */,
D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */, D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */,
BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */, BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */,
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */, BF66EE962501AEBC007EE018 /* ALTPatreonBenefitID.m in Sources */,
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */, BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */, D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */,
BF66EEE92501AED0007EE018 /* JSONDecoder+Properties.swift in Sources */, BF66EEE92501AED0007EE018 /* JSONDecoder+Properties.swift in Sources */,
@@ -3147,6 +3154,7 @@
BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */, BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */,
0EE7FDC92BE8D07400D1E390 /* NSError+AltStore.swift in Sources */, 0EE7FDC92BE8D07400D1E390 /* NSError+AltStore.swift in Sources */,
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */, D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */,
D5A645232AF5B5C50047D980 /* PatreonAPI+Responses.swift in Sources */,
D5185B822AE1E71D00646E33 /* Source13To14MigrationPolicy.swift in Sources */, D5185B822AE1E71D00646E33 /* Source13To14MigrationPolicy.swift in Sources */,
BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */, BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */,
BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */, BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */,

View File

@@ -18,7 +18,7 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
#import <AltStoreCore/ALTAppPermissions.h> #import <AltStoreCore/ALTAppPermissions.h>
#import <AltStoreCore/ALTSourceUserInfoKey.h> #import <AltStoreCore/ALTSourceUserInfoKey.h>
#import <AltStoreCore/ALTPatreonBenefitType.h> #import <AltStoreCore/ALTPatreonBenefitID.h>
// Shared // Shared
#import <AltStoreCore/ALTConstants.h> #import <AltStoreCore/ALTConstants.h>

View File

@@ -19,7 +19,7 @@ public class ManagedPatron: NSManagedObject, Fetchable
super.init(entity: entity, insertInto: context) 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. // Only cache Patrons with non-nil names.
guard let name = patron.name else { return nil } guard let name = patron.name else { return nil }

View File

@@ -8,27 +8,6 @@
import CoreData 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) @objc(PatreonAccount)
public class PatreonAccount: NSManagedObject, Fetchable public class PatreonAccount: NSManagedObject, Fetchable
{ {
@@ -44,13 +23,13 @@ public class PatreonAccount: NSManagedObject, Fetchable
super.init(entity: entity, insertInto: context) 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) super.init(entity: PatreonAccount.entity(), insertInto: context)
self.identifier = response.data.id self.identifier = account.identifier
self.name = response.data.attributes.full_name self.name = account.name
self.firstName = response.data.attributes.first_name self.firstName = account.firstName
// if let patronResponse = response.included?.first // if let patronResponse = response.included?.first
// { // {

View File

@@ -10,18 +10,25 @@ import Foundation
extension PatreonAPI 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 public struct Benefit: Hashable
init(response: PatreonAPI.BenefitResponse)
{ {
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)
}
} }
} }

View File

@@ -10,18 +10,25 @@ import Foundation
extension PatreonAPI 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 public struct Campaign
init(response: PatreonAPI.CampaignResponse)
{ {
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
}
} }
} }

View 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)
}
}
}
}

View File

@@ -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 extension PatreonAPI
{ {
static let altstoreCampaignID = "2863968"
typealias FetchAccountResponse = Response<UserAccountResponse>
typealias FriendZonePatronsResponse = Response<[PatronResponse]>
enum AuthorizationType enum AuthorizationType
{ {
case none case none
case user case user
case creator 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 public class PatreonAPI: NSObject
@@ -155,14 +114,17 @@ public extension PatreonAPI
func fetchAccount(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void) func fetchAccount(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
{ {
var components = URLComponents(string: "/api/oauth2/v2/identity")! 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[user]", value: "first_name,full_name"),
URLQueryItem(name: "fields[member]", value: "full_name,patron_status")] 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 requestURL = components.url(relativeTo: self.baseURL)!
let request = URLRequest(url: requestURL) 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 switch result
{ {
case .failure(~PatreonAPIErrorCode.notAuthenticated): case .failure(~PatreonAPIErrorCode.notAuthenticated):
@@ -170,10 +132,15 @@ public extension PatreonAPI
completion(.failure(PatreonAPIError(.notAuthenticated))) 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): case .success(let response):
let account = PatreonAPI.UserAccount(response: response.data, including: response.included)
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in 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 Keychain.shared.patreonAccountID = account.identifier
completion(.success(account)) completion(.success(account))
} }
@@ -183,57 +150,34 @@ public extension PatreonAPI
func fetchPatrons(completion: @escaping (Result<[Patron], Swift.Error>) -> Void) 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"), components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"),
URLQueryItem(name: "fields[tier]", value: "title"), URLQueryItem(name: "fields[tier]", value: "title"),
URLQueryItem(name: "fields[benefit]", value: "title"),
URLQueryItem(name: "fields[member]", value: "full_name,patron_status"), URLQueryItem(name: "fields[member]", value: "full_name,patron_status"),
URLQueryItem(name: "page[size]", value: "1000")] URLQueryItem(name: "page[size]", value: "1000")]
let requestURL = components.url(relativeTo: self.baseURL)! let requestURL = components.url(relativeTo: self.baseURL)!
struct Response: Decodable
{
var data: [PatronResponse]
var included: [AnyResponse]
var links: [String: URL]?
}
var allPatrons = [Patron]() var allPatrons = [Patron]()
func fetchPatrons(url: URL) func fetchPatrons(url: URL)
{ {
let request = URLRequest(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 switch result
{ {
case .failure(let error): completion(.failure(error)) case .failure(let error): completion(.failure(error))
case .success(let response): case .success(let patronsResponse):
let tiers = response.included.compactMap { (response) -> Tier? in let patrons = patronsResponse.data.map { (response) -> Patron in
switch response let patron = Patron(response: response, including: patronsResponse.included)
{
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)
}
return patron return patron
}.filter { $0.benefits.contains(where: { $0.type == .credits }) } }.filter { $0.benefits.contains(where: { $0.identifier == .credits }) }
allPatrons.append(contentsOf: patrons) allPatrons.append(contentsOf: patrons)
if let nextURL = response.links?["next"] if let nextURL = patronsResponse.links?["next"]
{ {
fetchPatrons(url: nextURL) fetchPatrons(url: nextURL)
} }

View File

@@ -10,38 +10,22 @@ import Foundation
extension PatreonAPI 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 PatronRelationships: Decodable
{
struct Relationships: Decodable var campaign: Response<AnyItemResponse>?
{ var currently_entitled_tiers: Response<[AnyItemResponse]>?
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?
} }
} }
extension Patron extension PatreonAPI
{ {
public enum Status: String, Decodable public enum Status: String, Decodable
{ {
@@ -50,29 +34,46 @@ extension Patron
case former = "former_patron" case former = "former_patron"
case unknown = "unknown" case unknown = "unknown"
} }
}
public class Patron
{
public var name: String?
public var identifier: String
public var status: Status // Roughly equivalent to AltStoreCore.Pledge
public class Patron
public var benefits: Set<Benefit> = []
init(response: PatreonAPI.PatronResponse)
{ {
self.name = response.attributes.full_name public var name: String?
self.identifier = response.id 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 self.name = response.attributes.full_name
} self.identifier = response.id
else
{ if let status = response.attributes.patron_status
self.status = .unknown {
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)
} }
} }
} }

View File

@@ -10,41 +10,38 @@ import Foundation
extension PatreonAPI extension PatreonAPI
{ {
struct TierResponse: Decodable typealias TierResponse = DataResponse<TierAttributes, TierRelationships>
struct TierAttributes: Decodable
{ {
struct Attributes: Decodable var title: String
{ }
var title: String
} struct TierRelationships: Decodable
{
struct Relationships: Decodable var benefits: Response<[AnyItemResponse]>?
{
struct Benefits: Decodable
{
var data: [BenefitResponse]
}
var benefits: Benefits
}
var id: String
var attributes: Attributes
var relationships: Relationships
} }
} }
public struct Tier extension PatreonAPI
{ {
public var name: String public struct Tier: Hashable
public var identifier: String
public var benefits: [Benefit] = []
init(response: PatreonAPI.TierResponse)
{ {
self.name = response.attributes.title public var name: String
self.identifier = response.id public var identifier: String
self.benefits = response.relationships.benefits.data.map(Benefit.init(response:))
// 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
}
} }
} }

View 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
}
}
}

View 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;

View 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";

View File

@@ -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;

View File

@@ -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";