mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[AltStoreCore] Adds Pledge, PledgeReward, and PledgeTier
Allows us to cache pledges for current user, which can be used to determine if user has access to Patreon-only apps.
This commit is contained in:
@@ -374,6 +374,9 @@
|
||||
D552EB062AF453F900A3AB4D /* URL+Normalized.swift in Sources */ = {isa = PBXBuildFile; fileRef = D552EB052AF453F900A3AB4D /* URL+Normalized.swift */; };
|
||||
D55467B82A8D5E2600F4CE90 /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */; };
|
||||
D55467C52A8D72C300F4CE90 /* ActiveAppsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */; };
|
||||
D557A4812AE85BB0007D0DCF /* Pledge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D557A4802AE85BB0007D0DCF /* Pledge.swift */; };
|
||||
D557A4832AE85DB7007D0DCF /* PledgeReward.swift in Sources */ = {isa = PBXBuildFile; fileRef = D557A4822AE85DB7007D0DCF /* PledgeReward.swift */; };
|
||||
D557A4852AE88227007D0DCF /* PledgeTier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D557A4842AE88227007D0DCF /* PledgeTier.swift */; };
|
||||
D561B2EB28EF5A4F006752E4 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; };
|
||||
D561B2ED28EF5A4F006752E4 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
D56915072AD5E91B00A2B747 /* Regex+Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */; };
|
||||
@@ -1047,6 +1050,9 @@
|
||||
D552EB052AF453F900A3AB4D /* URL+Normalized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Normalized.swift"; sourceTree = "<group>"; };
|
||||
D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = "<group>"; };
|
||||
D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsWidget.swift; sourceTree = "<group>"; };
|
||||
D557A4802AE85BB0007D0DCF /* Pledge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pledge.swift; sourceTree = "<group>"; };
|
||||
D557A4822AE85DB7007D0DCF /* PledgeReward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgeReward.swift; sourceTree = "<group>"; };
|
||||
D557A4842AE88227007D0DCF /* PledgeTier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgeTier.swift; sourceTree = "<group>"; };
|
||||
D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Regex+Permissions.swift"; sourceTree = "<group>"; };
|
||||
D56915082AD5F3E800A2B747 /* AltTests+Sources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AltTests+Sources.swift"; sourceTree = "<group>"; };
|
||||
D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = "<group>"; };
|
||||
@@ -1692,13 +1698,13 @@
|
||||
D58916FD28C7C55C00E39C8B /* LoggedError.swift */,
|
||||
BF66EEC52501AECA007EE018 /* MergePolicy.swift */,
|
||||
BF66EEBF2501AECA007EE018 /* NewsItem.swift */,
|
||||
BF66EEC82501AECA007EE018 /* PatreonAccount.swift */,
|
||||
D5CA0C4A280E141900469595 /* ManagedPatron.swift */,
|
||||
BF66EEC32501AECA007EE018 /* RefreshAttempt.swift */,
|
||||
BF66EEC12501AECA007EE018 /* SecureValueTransformer.swift */,
|
||||
BF66EEAB2501AECA007EE018 /* Source.swift */,
|
||||
BF66EEC42501AECA007EE018 /* StoreApp.swift */,
|
||||
BF66EEC22501AECA007EE018 /* Team.swift */,
|
||||
D557A4862AE88232007D0DCF /* Patreon */,
|
||||
BF66EEAC2501AECA007EE018 /* Migrations */,
|
||||
);
|
||||
path = Model;
|
||||
@@ -2247,6 +2253,17 @@
|
||||
path = "App Intents";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D557A4862AE88232007D0DCF /* Patreon */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF66EEC82501AECA007EE018 /* PatreonAccount.swift */,
|
||||
D557A4802AE85BB0007D0DCF /* Pledge.swift */,
|
||||
D557A4842AE88227007D0DCF /* PledgeTier.swift */,
|
||||
D557A4822AE85DB7007D0DCF /* PledgeReward.swift */,
|
||||
);
|
||||
path = Patreon;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D55FEC9C2A8FEC600057D6E6 /* Legacy */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -3117,6 +3134,7 @@
|
||||
BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */,
|
||||
0EE7FDC72BE8CF4100D1E390 /* ALTWrappedError.m in Sources */,
|
||||
D5FD4ECB2A9532960097BEE8 /* DatabaseManager+Async.swift in Sources */,
|
||||
D557A4832AE85DB7007D0DCF /* PledgeReward.swift in Sources */,
|
||||
D5893F802A1419E800E767CD /* NSManagedObjectContext+Conveniences.swift in Sources */,
|
||||
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */,
|
||||
D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */,
|
||||
@@ -3166,6 +3184,8 @@
|
||||
BF66EED62501AECA007EE018 /* NewsItem.swift in Sources */,
|
||||
BF66EEA72501AEC5007EE018 /* Campaign.swift in Sources */,
|
||||
BF66EE992501AEBC007EE018 /* ALTSourceUserInfoKey.m in Sources */,
|
||||
D557A4812AE85BB0007D0DCF /* Pledge.swift in Sources */,
|
||||
D557A4852AE88227007D0DCF /* PledgeTier.swift in Sources */,
|
||||
BF989185250AAD1D002ACF50 /* UIColor+AltStore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="pledges" toMany="YES" deletionRule="Cascade" destinationEntity="Pledge" inverseName="account" inverseEntity="Pledge"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
@@ -169,6 +170,40 @@
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Pledge" representedClassName="Pledge" syncable="YES">
|
||||
<attribute name="amount" attributeType="Decimal" defaultValueString="0"/>
|
||||
<attribute name="campaignURL" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PatreonAccount" inverseName="pledges" inverseEntity="PatreonAccount"/>
|
||||
<relationship name="rewards" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="PledgeReward" inverseName="pledge" inverseEntity="PledgeReward"/>
|
||||
<relationship name="tiers" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="PledgeTier" inverseName="pledge" inverseEntity="PledgeTier"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PledgeReward" representedClassName="PledgeReward" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="pledge" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Pledge" inverseName="rewards" inverseEntity="Pledge"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PledgeTier" representedClassName="PledgeTier" syncable="YES">
|
||||
<attribute name="amount" attributeType="Decimal" defaultValueString="0.0"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="pledge" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Pledge" inverseName="tiers" inverseEntity="Pledge"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
|
||||
@@ -262,7 +262,36 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
{
|
||||
featuredAppIDsBySourceID[databaseObject.identifier] = contextSource.featuredApps?.map { $0.bundleIdentifier }
|
||||
}
|
||||
|
||||
|
||||
case let databasePledge as Pledge:
|
||||
guard let contextPledge = conflict.conflictingObjects.first as? Pledge else { break }
|
||||
|
||||
// Tiers
|
||||
let contextTierIDs = Set(contextPledge._tiers.lazy.compactMap { $0 as? PledgeTier }.map { $0.identifier })
|
||||
for case let databaseTier as PledgeTier in databasePledge._tiers where !contextTierIDs.contains(databaseTier.identifier)
|
||||
{
|
||||
// Tier ID does NOT exist in context, so delete existing databaseTier.
|
||||
databaseTier.managedObjectContext?.delete(databaseTier)
|
||||
}
|
||||
|
||||
// Rewards
|
||||
let contextRewardIDs = Set(contextPledge._rewards.lazy.compactMap { $0 as? PledgeReward }.map { $0.identifier })
|
||||
for case let databaseReward as PledgeReward in databasePledge._rewards where !contextRewardIDs.contains(databaseReward.identifier)
|
||||
{
|
||||
// Reward ID does NOT exist in context, so delete existing databaseReward.
|
||||
databaseReward.managedObjectContext?.delete(databaseReward)
|
||||
}
|
||||
|
||||
case let databaseAccount as PatreonAccount:
|
||||
guard let contextAccount = conflict.conflictingObjects.first as? PatreonAccount else { break }
|
||||
|
||||
let contextPledgeIDs = Set(contextAccount._pledges.lazy.compactMap { $0 as? Pledge }.map { $0.identifier })
|
||||
for case let databasePledge as Pledge in databaseAccount._pledges where !contextPledgeIDs.contains(databasePledge.identifier)
|
||||
{
|
||||
// Pledge ID does NOT exist in context, so delete existing databasePledge.
|
||||
databasePledge.managedObjectContext?.delete(databasePledge)
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ public class PatreonAccount: NSManagedObject, Fetchable
|
||||
|
||||
@NSManaged public var isPatron: Bool
|
||||
|
||||
/* Relationships */
|
||||
@nonobjc public var pledges: Set<Pledge> { _pledges as! Set<Pledge> }
|
||||
@NSManaged @objc(pledges) internal var _pledges: NSSet
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
54
AltStoreCore/Model/Patreon/Pledge.swift
Normal file
54
AltStoreCore/Model/Patreon/Pledge.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Pledge.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 10/24/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(Pledge)
|
||||
public class Pledge: NSManagedObject, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var identifier: String
|
||||
@NSManaged public private(set) var campaignURL: URL
|
||||
|
||||
@nonobjc public var amount: Decimal { _amount as Decimal }
|
||||
@NSManaged @objc(amount) private var _amount: NSDecimalNumber
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var account: PatreonAccount?
|
||||
|
||||
@nonobjc public var tiers: Set<PledgeTier> { _tiers as! Set<PledgeTier> }
|
||||
@NSManaged @objc(tiers) internal var _tiers: NSSet
|
||||
|
||||
@nonobjc public var rewards: Set<PledgeReward> { _rewards as! Set<PledgeReward> }
|
||||
@NSManaged @objc(rewards) internal var _rewards: NSSet
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init?(patron: PatreonAPI.Patron, context: NSManagedObjectContext)
|
||||
{
|
||||
guard let amount = patron.pledgeAmount, let campaignURL = patron.campaign?.url else { return nil }
|
||||
|
||||
super.init(entity: Pledge.entity(), insertInto: context)
|
||||
|
||||
self.identifier = patron.identifier
|
||||
self._amount = amount as NSDecimalNumber
|
||||
self.campaignURL = campaignURL
|
||||
}
|
||||
}
|
||||
|
||||
public extension Pledge
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Pledge>
|
||||
{
|
||||
return NSFetchRequest<Pledge>(entityName: "Pledge")
|
||||
}
|
||||
}
|
||||
42
AltStoreCore/Model/Patreon/PledgeReward.swift
Normal file
42
AltStoreCore/Model/Patreon/PledgeReward.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// PledgeReward.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 10/24/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(PledgeReward)
|
||||
public class PledgeReward: NSManagedObject, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var name: String
|
||||
@NSManaged public private(set) var identifier: String
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var pledge: Pledge?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(benefit: PatreonAPI.Benefit, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: PledgeReward.entity(), insertInto: context)
|
||||
|
||||
self.name = benefit.name
|
||||
self.identifier = benefit.identifier.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
public extension PledgeReward
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<PledgeReward>
|
||||
{
|
||||
return NSFetchRequest<PledgeReward>(entityName: "PledgeReward")
|
||||
}
|
||||
}
|
||||
46
AltStoreCore/Model/Patreon/PledgeTier.swift
Normal file
46
AltStoreCore/Model/Patreon/PledgeTier.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// PledgeTier.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 10/24/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(PledgeTier)
|
||||
public class PledgeTier: NSManagedObject, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged public private(set) var name: String
|
||||
@NSManaged public private(set) var identifier: String
|
||||
|
||||
@nonobjc public var amount: Decimal { _amount as Decimal } // In USD
|
||||
@NSManaged @objc(amount) private var _amount: NSDecimalNumber
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged public private(set) var pledge: Pledge?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(tier: PatreonAPI.Tier, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: PledgeTier.entity(), insertInto: context)
|
||||
|
||||
self.name = tier.name
|
||||
self.identifier = tier.identifier
|
||||
self._amount = tier.amount as NSDecimalNumber
|
||||
}
|
||||
}
|
||||
|
||||
public extension PledgeTier
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<PledgeTier>
|
||||
{
|
||||
return NSFetchRequest<PledgeTier>(entityName: "PledgeTier")
|
||||
}
|
||||
}
|
||||
@@ -116,10 +116,10 @@ public extension PatreonAPI
|
||||
var components = URLComponents(string: "/api/oauth2/v2/identity")!
|
||||
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[tier]", value: "title,amount_cents"),
|
||||
URLQueryItem(name: "fields[benefit]", value: "title"),
|
||||
URLQueryItem(name: "fields[campaign]", value: "url"),
|
||||
URLQueryItem(name: "fields[member]", value: "full_name,patron_status,currently_entitled_amount_cents")]
|
||||
|
||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
||||
let request = URLRequest(url: requestURL)
|
||||
@@ -152,9 +152,9 @@ public extension PatreonAPI
|
||||
{
|
||||
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[tier]", value: "title,amount_cents"),
|
||||
URLQueryItem(name: "fields[benefit]", value: "title"),
|
||||
URLQueryItem(name: "fields[member]", value: "full_name,patron_status"),
|
||||
URLQueryItem(name: "fields[member]", value: "full_name,patron_status,currently_entitled_amount_cents"),
|
||||
URLQueryItem(name: "page[size]", value: "1000")]
|
||||
|
||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
||||
|
||||
@@ -16,6 +16,7 @@ extension PatreonAPI
|
||||
{
|
||||
var full_name: String?
|
||||
var patron_status: String?
|
||||
var currently_entitled_amount_cents: Int32 // In campaign's currency
|
||||
}
|
||||
|
||||
struct PatronRelationships: Decodable
|
||||
@@ -40,6 +41,7 @@ extension PatreonAPI
|
||||
{
|
||||
public var name: String?
|
||||
public var identifier: String
|
||||
public var pledgeAmount: Decimal?
|
||||
public var status: Status
|
||||
|
||||
// Relationships
|
||||
@@ -51,6 +53,7 @@ extension PatreonAPI
|
||||
{
|
||||
self.name = response.attributes.full_name
|
||||
self.identifier = response.id
|
||||
self.pledgeAmount = Decimal(response.attributes.currently_entitled_amount_cents) / 100
|
||||
|
||||
if let status = response.attributes.patron_status
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ extension PatreonAPI
|
||||
struct TierAttributes: Decodable
|
||||
{
|
||||
var title: String
|
||||
var amount_cents: Int32 // In USD
|
||||
}
|
||||
|
||||
struct TierRelationships: Decodable
|
||||
@@ -29,6 +30,7 @@ extension PatreonAPI
|
||||
{
|
||||
public var name: String
|
||||
public var identifier: String
|
||||
public var amount: Decimal
|
||||
|
||||
// Relationships
|
||||
public var benefits: [Benefit] = []
|
||||
@@ -38,6 +40,9 @@ extension PatreonAPI
|
||||
self.name = response.attributes.title
|
||||
self.identifier = response.id
|
||||
|
||||
let amount = Decimal(response.attributes.amount_cents) / 100
|
||||
self.amount = amount
|
||||
|
||||
guard let included, let benefitIDs = response.relationships?.benefits?.data.map(\.id) else { return }
|
||||
|
||||
let benefits = benefitIDs.compactMap { included.benefits[$0] }.map(Benefit.init(response:))
|
||||
|
||||
Reference in New Issue
Block a user