mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[AltStore] Adds basic Patreon integration
- Lists beta versions of apps when signed in to Patreon - Lists names of Patrons with the Credits benefit
This commit is contained in:
@@ -104,6 +104,7 @@
|
||||
BF4588882298DD3F00BD7491 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4588872298DD3F00BD7491 /* libxml2.tbd */; };
|
||||
BF4713A522976D1E00784A2F /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; };
|
||||
BF4713A622976D1E00784A2F /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */; };
|
||||
BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; };
|
||||
BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; };
|
||||
@@ -169,6 +170,13 @@
|
||||
BFD52C2022A1A9EC000B7ED1 /* node.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1D22A1A9EC000B7ED1 /* node.c */; };
|
||||
BFD52C2122A1A9EC000B7ED1 /* node_list.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1E22A1A9EC000B7ED1 /* node_list.c */; };
|
||||
BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; };
|
||||
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6E7230CC961007955AB /* PatreonAPI.swift */; };
|
||||
BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */; };
|
||||
BFD5D6EC230CCDA1007955AB /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6EB230CCDA1007955AB /* PatreonViewController.swift */; };
|
||||
BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6ED230D8A86007955AB /* Patron.swift */; };
|
||||
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F1230DD974007955AB /* Benefit.swift */; };
|
||||
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F3230DDB0A007955AB /* Campaign.swift */; };
|
||||
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F5230DDB12007955AB /* Tier.swift */; };
|
||||
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */; };
|
||||
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */; };
|
||||
BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */; };
|
||||
@@ -356,6 +364,8 @@
|
||||
BF4588872298DD3F00BD7491 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
BF4588962298DE6E00BD7491 /* libzip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libzip.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF4713A422976CFC00784A2F /* openssl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = openssl.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = "<group>"; };
|
||||
BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = "<group>"; };
|
||||
BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = "<group>"; };
|
||||
BF770E5322BC044E002A40FE /* AppOperationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOperationContext.swift; sourceTree = "<group>"; };
|
||||
@@ -424,6 +434,13 @@
|
||||
BFD52C1D22A1A9EC000B7ED1 /* node.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node.c; path = Dependencies/libplist/libcnary/node.c; sourceTree = SOURCE_ROOT; };
|
||||
BFD52C1E22A1A9EC000B7ED1 /* node_list.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node_list.c; path = Dependencies/libplist/libcnary/node_list.c; sourceTree = SOURCE_ROOT; };
|
||||
BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; };
|
||||
BFD5D6E7230CC961007955AB /* PatreonAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonAPI.swift; sourceTree = "<group>"; };
|
||||
BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonAccount.swift; sourceTree = "<group>"; };
|
||||
BFD5D6EB230CCDA1007955AB /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = "<group>"; };
|
||||
BFD5D6ED230D8A86007955AB /* Patron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patron.swift; sourceTree = "<group>"; };
|
||||
BFD5D6F1230DD974007955AB /* Benefit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benefit.swift; sourceTree = "<group>"; };
|
||||
BFD5D6F3230DDB0A007955AB /* Campaign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Campaign.swift; sourceTree = "<group>"; };
|
||||
BFD5D6F5230DDB12007955AB /* Tier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tier.swift; sourceTree = "<group>"; };
|
||||
BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsComponents.swift; sourceTree = "<group>"; };
|
||||
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+RelativeDate.swift"; sourceTree = "<group>"; };
|
||||
BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BrowseCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
@@ -515,6 +532,8 @@
|
||||
children = (
|
||||
BF3D648B22E79AC800E9056B /* ALTAppPermission.h */,
|
||||
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */,
|
||||
BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */,
|
||||
BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */,
|
||||
);
|
||||
path = Types;
|
||||
sourceTree = "<group>";
|
||||
@@ -777,6 +796,7 @@
|
||||
BF3D64A022E7FAD800E9056B /* App Detail */,
|
||||
BFBBE2E2229320A2002097FA /* My Apps */,
|
||||
BFDB69FB22A9A7A6007EA6D6 /* Settings */,
|
||||
BFD5D6E6230CC94B007955AB /* Patreon */,
|
||||
BFD2478A2284C49000981D42 /* Managing Apps */,
|
||||
BFC51D7922972F1F00388324 /* Server */,
|
||||
BFD247982284D7FC00981D42 /* Model */,
|
||||
@@ -859,6 +879,7 @@
|
||||
BFBBE2DE22931F73002097FA /* App.swift */,
|
||||
BF3D648722E79A3700E9056B /* AppPermission.swift */,
|
||||
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
|
||||
BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */,
|
||||
BF02419322F2156E00129732 /* RefreshAttempt.swift */,
|
||||
BFE338DC22F0E7F3002E24B9 /* Source.swift */,
|
||||
BFE6326522A857C100F30809 /* Team.swift */,
|
||||
@@ -887,11 +908,24 @@
|
||||
path = Connections;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFD5D6E6230CC94B007955AB /* Patreon */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFD5D6E7230CC961007955AB /* PatreonAPI.swift */,
|
||||
BFD5D6ED230D8A86007955AB /* Patron.swift */,
|
||||
BFD5D6F3230DDB0A007955AB /* Campaign.swift */,
|
||||
BFD5D6F5230DDB12007955AB /* Tier.swift */,
|
||||
BFD5D6F1230DD974007955AB /* Benefit.swift */,
|
||||
);
|
||||
path = Patreon;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFDB69FB22A9A7A6007EA6D6 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFDB69FC22A9A7B7007EA6D6 /* SettingsViewController.swift */,
|
||||
BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */,
|
||||
BFD5D6EB230CCDA1007955AB /* PatreonViewController.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1290,21 +1324,27 @@
|
||||
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
|
||||
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
|
||||
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */,
|
||||
BFD5D6EC230CCDA1007955AB /* PatreonViewController.swift in Sources */,
|
||||
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */,
|
||||
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
|
||||
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */,
|
||||
BFDB69FD22A9A7B7007EA6D6 /* SettingsViewController.swift in Sources */,
|
||||
BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */,
|
||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
|
||||
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
|
||||
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */,
|
||||
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
|
||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
||||
BFBBE2DF22931F73002097FA /* App.swift in Sources */,
|
||||
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
|
||||
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */,
|
||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
||||
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
|
||||
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */,
|
||||
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
||||
BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */,
|
||||
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
|
||||
BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */,
|
||||
BFE6326822A858F300F30809 /* Account.swift in Sources */,
|
||||
BFE6326622A857C200F30809 /* Team.swift in Sources */,
|
||||
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
|
||||
@@ -1319,6 +1359,7 @@
|
||||
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */,
|
||||
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */,
|
||||
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */,
|
||||
BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */,
|
||||
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
|
||||
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
|
||||
BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */,
|
||||
@@ -1335,6 +1376,7 @@
|
||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
||||
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */,
|
||||
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */,
|
||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
||||
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "ALTAppPermission.h"
|
||||
#import "ALTPatreonBenefitType.h"
|
||||
|
||||
@@ -37,6 +37,7 @@ class AppViewController: UIViewController
|
||||
@IBOutlet private var developerLabel: UILabel!
|
||||
@IBOutlet private var downloadButton: PillButton!
|
||||
@IBOutlet private var appIconImageView: UIImageView!
|
||||
@IBOutlet private var betaBadgeView: UIImageView!
|
||||
|
||||
@IBOutlet private var backgroundAppIconImageView: UIImageView!
|
||||
@IBOutlet private var backgroundBlurView: UIVisualEffectView!
|
||||
@@ -88,6 +89,7 @@ class AppViewController: UIViewController
|
||||
self.appIconImageView.image = nil
|
||||
self.appIconImageView.tintColor = self.app.tintColor
|
||||
self.downloadButton.tintColor = self.app.tintColor
|
||||
self.betaBadgeView.isHidden = !self.app.isBeta
|
||||
|
||||
self.backButtonContainerView.tintColor = self.app.tintColor
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
{
|
||||
AppManager.shared.update()
|
||||
ServerManager.shared.startDiscovering()
|
||||
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="LZw-eU-5SO" userLabel="App Info">
|
||||
<rect key="frame" x="0.0" y="0.0" width="273" height="93"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="93"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="3Ey-6S-HJx" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||
@@ -143,17 +143,25 @@
|
||||
<constraint firstAttribute="width" secondItem="3Ey-6S-HJx" secondAttribute="height" multiplier="1:1" id="GCk-a1-dDk"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="bR7-SO-m8f">
|
||||
<rect key="frame" x="90" y="26.5" width="88" height="40.5"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="bR7-SO-m8f">
|
||||
<rect key="frame" x="90" y="26.5" width="135" height="40.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dNE-IO-y3o">
|
||||
<rect key="frame" x="0.0" y="0.0" width="88" height="21.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="9z7-I4-q6g">
|
||||
<rect key="frame" x="0.0" y="0.0" width="135" height="21.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="dNE-IO-y3o">
|
||||
<rect key="frame" x="0.0" y="0.0" width="88" height="21.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="2XC-Fe-yG4">
|
||||
<rect key="frame" x="94" y="0.0" width="41" height="21.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NKT-el-rRF">
|
||||
<rect key="frame" x="0.0" y="23.5" width="88" height="17"/>
|
||||
<rect key="frame" x="0.0" y="23.5" width="66" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -161,7 +169,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mgB-Gs-bik" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="189" y="31" width="72" height="31"/>
|
||||
<rect key="frame" x="236" y="31" width="72" height="31"/>
|
||||
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="72" id="j44-T1-0dc"/>
|
||||
@@ -246,6 +254,7 @@
|
||||
<outlet property="backButtonContainerView" destination="tUK-0J-07U" id="POZ-dP-f12"/>
|
||||
<outlet property="backgroundAppIconImageView" destination="CUB-SN-zdM" id="dFx-py-yMm"/>
|
||||
<outlet property="backgroundBlurView" destination="8Tg-wk-r0u" id="B8c-ng-nI5"/>
|
||||
<outlet property="betaBadgeView" destination="2XC-Fe-yG4" id="FCf-t9-Aab"/>
|
||||
<outlet property="contentView" destination="Qlg-m3-lXg" id="JhH-hh-vBN"/>
|
||||
<outlet property="developerLabel" destination="NKT-el-rRF" id="GUc-jy-kvv"/>
|
||||
<outlet property="downloadButton" destination="mgB-Gs-bik" id="x95-gu-NBy"/>
|
||||
@@ -693,10 +702,34 @@ World</string>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Patreon" id="XP4-Fa-8aU">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="gray" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="5YC-85-5D3" style="IBUITableViewCellStyleDefault" id="MUH-Rw-1Bv">
|
||||
<rect key="frame" x="0.0" y="299.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="MUH-Rw-1Bv" id="aVM-dk-WjN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="View Account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="5YC-85-5D3">
|
||||
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="Qcm-t0-PIR" kind="show" id="qdd-iP-BIQ"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Debug" id="K7R-6x-gHl">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="nQj-qq-PmI" style="IBUITableViewCellStyleDefault" id="8M3-mu-gRd">
|
||||
<rect key="frame" x="0.0" y="299.5" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="399.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="8M3-mu-gRd" id="6TQ-nF-Rkl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
|
||||
@@ -821,6 +854,60 @@ World</string>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2313" y="515"/>
|
||||
</scene>
|
||||
<!--Patreon-->
|
||||
<scene sceneID="rWf-MO-Hyc">
|
||||
<objects>
|
||||
<tableViewController id="Qcm-t0-PIR" customClass="PatreonViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="4tt-fZ-vas">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="N6C-v5-zxL" style="IBUITableViewCellStyleDefault" id="DuT-r1-9Cy">
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="DuT-r1-9Cy" id="OvI-oA-W0V">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="N6C-v5-zxL">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="Qcm-t0-PIR" id="dtj-fZ-zJz"/>
|
||||
<outlet property="delegate" destination="Qcm-t0-PIR" id="Qds-j3-bH2"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Patreon" largeTitleDisplayMode="never" id="aiq-EO-aN7">
|
||||
<barButtonItem key="rightBarButtonItem" title="Sign In" style="done" id="dBD-5X-mrn">
|
||||
<connections>
|
||||
<action selector="authenticate:" destination="Qcm-t0-PIR" id="oIV-a5-OLo"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="signInButton" destination="dBD-5X-mrn" id="vo7-1j-hxN"/>
|
||||
<outlet property="signOutButton" destination="RTm-wb-5aT" id="W0P-5W-L3c"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="BAy-cw-HqZ" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<barButtonItem title="Sign Out" style="done" id="RTm-wb-5aT">
|
||||
<color key="tintColor" name="RefreshRed"/>
|
||||
<connections>
|
||||
<action selector="signOut:" destination="Qcm-t0-PIR" id="B0H-o6-3Cq"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2313" y="1193"/>
|
||||
</scene>
|
||||
<!--Browse-->
|
||||
<scene sceneID="VHa-uP-bFU">
|
||||
<objects>
|
||||
@@ -894,17 +981,25 @@ World</string>
|
||||
<constraint firstAttribute="width" secondItem="H12-ip-Bbl" secondAttribute="height" multiplier="1:1" id="ZIR-f8-Jc4"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="7iy-Zp-LEj">
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="7iy-Zp-LEj">
|
||||
<rect key="frame" x="71" y="12" width="203" height="36"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Short" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Nhl-6I-9gW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="203" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="MRz-3W-aTM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="85" height="18"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="Short" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Nhl-6I-9gW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="38" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="mtL-iA-JnD">
|
||||
<rect key="frame" x="44" y="0.0" width="41" height="18"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Hp4-uP-55T">
|
||||
<rect key="frame" x="0.0" y="20" width="203" height="16"/>
|
||||
<rect key="frame" x="0.0" y="20" width="62" height="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -936,6 +1031,7 @@ World</string>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="appIconImageView" destination="H12-ip-Bbl" id="61F-4i-4Q3"/>
|
||||
<outlet property="betaBadgeView" destination="mtL-iA-JnD" id="v8W-bc-EB7"/>
|
||||
<outlet property="developerLabel" destination="Hp4-uP-55T" id="Cqx-3O-knq"/>
|
||||
<outlet property="nameLabel" destination="Nhl-6I-9gW" id="lzd-pp-PEQ"/>
|
||||
<outlet property="refreshButton" destination="dh4-fU-DFx" id="KWX-9y-2w8"/>
|
||||
@@ -1015,12 +1111,16 @@ World</string>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Back" width="18" height="18"/>
|
||||
<image name="BetaBadge" width="41" height="17"/>
|
||||
<image name="Browse" width="19.5" height="20.5"/>
|
||||
<image name="MyApps" width="28" height="24"/>
|
||||
<image name="Settings" width="21" height="21"/>
|
||||
<namedColor name="Green">
|
||||
<color red="0.22352941176470589" green="0.49411764705882355" blue="0.396078431372549" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="RefreshRed">
|
||||
<color red="1" green="0.23137254901960785" blue="0.18823529411764706" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="cnd-KK-o60"/>
|
||||
|
||||
@@ -28,6 +28,7 @@ import Nuke
|
||||
@IBOutlet var subtitleLabel: UILabel!
|
||||
|
||||
@IBOutlet var screenshotsCollectionView: UICollectionView!
|
||||
@IBOutlet var betaBadgeView: UIImageView!
|
||||
|
||||
@IBOutlet private var screenshotsContentView: UIView!
|
||||
|
||||
|
||||
@@ -29,17 +29,25 @@
|
||||
<constraint firstAttribute="height" constant="65" id="ufl-3d-nkT"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="zkp-KH-OyV">
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="zkp-KH-OyV">
|
||||
<rect key="frame" x="76" y="21" width="176" height="37"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xni-8I-ewW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="176" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Ykl-yo-ncv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="127.5" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="xni-8I-ewW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="80.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="5gN-I2-QOB">
|
||||
<rect key="frame" x="86.5" y="0.0" width="41" height="20.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="B5S-HI-tWJ">
|
||||
<rect key="frame" x="0.0" y="22.5" width="176" height="14.5"/>
|
||||
<rect key="frame" x="0.0" y="22.5" width="57.5" height="14.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -108,6 +116,7 @@
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="DeC-Y2-fvR" id="VDk-4D-STy"/>
|
||||
<outlet property="appIconImageView" destination="F2j-pX-09A" id="COe-74-adn"/>
|
||||
<outlet property="betaBadgeView" destination="5gN-I2-QOB" id="hu7-Ax-Wbc"/>
|
||||
<outlet property="developerLabel" destination="B5S-HI-tWJ" id="QGh-1g-fFv"/>
|
||||
<outlet property="nameLabel" destination="xni-8I-ewW" id="V56-ZT-vFa"/>
|
||||
<outlet property="screenshotsCollectionView" destination="RFs-qp-Ca4" id="xfi-AN-l17"/>
|
||||
@@ -116,4 +125,7 @@
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="BetaBadge" width="41" height="17"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -39,6 +39,7 @@ class BrowseViewController: UICollectionViewController
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.fetchSource()
|
||||
self.updateDataSource()
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||
@@ -80,6 +81,7 @@ private extension BrowseViewController
|
||||
cell.imageURLs = Array(app.screenshotURLs.prefix(2))
|
||||
cell.appIconImageView.image = nil
|
||||
cell.appIconImageView.isIndicatingActivity = true
|
||||
cell.betaBadgeView.isHidden = !app.isBeta
|
||||
|
||||
cell.actionButton.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
cell.actionButton.activityIndicatorView.style = .white
|
||||
@@ -138,6 +140,18 @@ private extension BrowseViewController
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func updateDataSource()
|
||||
{
|
||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron
|
||||
{
|
||||
self.dataSource.predicate = nil
|
||||
}
|
||||
else
|
||||
{
|
||||
self.dataSource.predicate = NSPredicate(format: "%K == NO", #keyPath(StoreApp.isBeta))
|
||||
}
|
||||
}
|
||||
|
||||
func fetchSource()
|
||||
{
|
||||
AppManager.shared.fetchSource() { (result) in
|
||||
|
||||
@@ -71,4 +71,24 @@ extension Keychain
|
||||
self.keychain["signingCertificateSerialNumber"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var patreonAccessToken: String? {
|
||||
get {
|
||||
let accessToken = try? self.keychain.get("patreonAccessToken")
|
||||
return accessToken
|
||||
}
|
||||
set {
|
||||
self.keychain["patreonAccessToken"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var patreonRefreshToken: String? {
|
||||
get {
|
||||
let refreshToken = try? self.keychain.get("patreonRefreshToken")
|
||||
return refreshToken
|
||||
}
|
||||
set {
|
||||
self.keychain["patreonRefreshToken"] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,16 @@
|
||||
<string>altstore-com.rileytestut.altstore</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltStore General</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altstore</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
@@ -38,6 +48,7 @@
|
||||
<string>altstore-com.rileytestut.AltStore</string>
|
||||
<string>altstore-com.rileytestut.Delta</string>
|
||||
<string>altstore-com.rileytestut.Clip</string>
|
||||
<string>altstore-com.rileytestut.AltStore.Beta</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
|
||||
@@ -45,6 +45,7 @@ extension LaunchViewController
|
||||
super.finishLaunching()
|
||||
|
||||
AppManager.shared.update()
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
|
||||
self.performSegue(withIdentifier: "finishLaunching", sender: nil)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,17 @@
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||
@@ -59,6 +70,7 @@
|
||||
<attribute name="developerName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="iconURL" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable" syncable="YES"/>
|
||||
@@ -94,9 +106,10 @@
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
|
||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="105"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="300"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="315"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -38,6 +38,7 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
|
||||
@NSManaged private(set) var downloadURL: URL
|
||||
@NSManaged private(set) var tintColor: UIColor?
|
||||
@NSManaged private(set) var isBeta: Bool
|
||||
|
||||
@NSManaged var sortIndex: Int32
|
||||
|
||||
@@ -71,6 +72,7 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
case subtitle
|
||||
case permissions
|
||||
case size
|
||||
case isBeta = "beta"
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws
|
||||
@@ -106,6 +108,7 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
}
|
||||
|
||||
self.size = try container.decode(Int32.self, forKey: .size)
|
||||
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||
|
||||
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
|
||||
|
||||
|
||||
@@ -115,6 +115,12 @@ extension DatabaseManager
|
||||
let activeTeam = Team.first(satisfying: predicate, in: context)
|
||||
return activeTeam
|
||||
}
|
||||
|
||||
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount?
|
||||
{
|
||||
let patronAccount = PatreonAccount.first(in: context)
|
||||
return patronAccount
|
||||
}
|
||||
}
|
||||
|
||||
private extension DatabaseManager
|
||||
|
||||
@@ -82,7 +82,16 @@ extension InstalledApp
|
||||
|
||||
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K != %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
var predicate = NSPredicate(format: "%K != %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
|
||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron
|
||||
{
|
||||
// No additional predicate
|
||||
}
|
||||
else
|
||||
{
|
||||
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "%K == NO", #keyPath(InstalledApp.storeApp.isBeta))])
|
||||
}
|
||||
|
||||
var installedApps = InstalledApp.all(satisfying: predicate,
|
||||
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
||||
@@ -102,10 +111,19 @@ extension InstalledApp
|
||||
// Date 6 hours before now.
|
||||
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
||||
|
||||
let predicate = NSPredicate(format: "(%K < %@) AND (%K != %@)",
|
||||
var predicate = NSPredicate(format: "(%K < %@) AND (%K != %@)",
|
||||
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
||||
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
|
||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron
|
||||
{
|
||||
// No additional predicate
|
||||
}
|
||||
else
|
||||
{
|
||||
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "%K == NO", #keyPath(InstalledApp.storeApp.isBeta))])
|
||||
}
|
||||
|
||||
var installedApps = InstalledApp.all(satisfying: predicate,
|
||||
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
||||
in: context)
|
||||
|
||||
74
AltStore/Model/PatreonAccount.swift
Normal file
74
AltStore/Model/PatreonAccount.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// PatreonAccount.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
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)
|
||||
class PatreonAccount: NSManagedObject, Fetchable
|
||||
{
|
||||
@NSManaged var identifier: String
|
||||
|
||||
@NSManaged var name: String
|
||||
@NSManaged var firstName: String?
|
||||
|
||||
@NSManaged var isPatron: Bool
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(response: PatreonAPI.AccountResponse, 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
|
||||
|
||||
if let patronResponse = response.included.first
|
||||
{
|
||||
let patron = Patron(response: patronResponse)
|
||||
self.isPatron = (patron.status == .active)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.isPatron = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PatreonAccount
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<PatreonAccount>
|
||||
{
|
||||
return NSFetchRequest<PatreonAccount>(entityName: "PatreonAccount")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class InstalledAppCollectionViewCell: UICollectionViewCell
|
||||
@IBOutlet var nameLabel: UILabel!
|
||||
@IBOutlet var developerLabel: UILabel!
|
||||
@IBOutlet var refreshButton: PillButton!
|
||||
@IBOutlet var betaBadgeView: UIImageView!
|
||||
}
|
||||
|
||||
class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
||||
|
||||
@@ -97,6 +97,13 @@ class MyAppsViewController: UICollectionViewController
|
||||
self.collectionView.addGestureRecognizer(self.longPressGestureRecognizer)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.updateDataSource()
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||
{
|
||||
guard segue.identifier == "showApp" else { return }
|
||||
@@ -163,6 +170,7 @@ private extension MyAppsViewController
|
||||
cell.versionDescriptionTextView.text = app.versionDescription
|
||||
cell.appIconImageView.image = nil
|
||||
cell.appIconImageView.isIndicatingActivity = true
|
||||
cell.betaBadgeView.isHidden = !app.isBeta
|
||||
|
||||
cell.updateButton.isIndicatingActivity = false
|
||||
cell.updateButton.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
|
||||
@@ -234,6 +242,7 @@ private extension MyAppsViewController
|
||||
let cell = cell as! InstalledAppCollectionViewCell
|
||||
cell.tintColor = tintColor
|
||||
cell.appIconImageView.isIndicatingActivity = true
|
||||
cell.betaBadgeView.isHidden = !(installedApp.storeApp?.isBeta ?? false)
|
||||
|
||||
cell.refreshButton.isIndicatingActivity = false
|
||||
cell.refreshButton.addTarget(self, action: #selector(MyAppsViewController.refreshApp(_:)), for: .primaryActionTriggered)
|
||||
@@ -295,6 +304,18 @@ private extension MyAppsViewController
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func updateDataSource()
|
||||
{
|
||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron
|
||||
{
|
||||
self.dataSource.predicate = nil
|
||||
}
|
||||
else
|
||||
{
|
||||
self.dataSource.predicate = NSPredicate(format: "%K != nil AND %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension MyAppsViewController
|
||||
|
||||
@@ -31,6 +31,7 @@ extension UpdateCollectionViewCell
|
||||
@IBOutlet var updateButton: PillButton!
|
||||
@IBOutlet var versionDescriptionTitleLabel: UILabel!
|
||||
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
|
||||
@IBOutlet var betaBadgeView: UIImageView!
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
|
||||
@@ -35,17 +35,25 @@
|
||||
<constraint firstAttribute="width" secondItem="jg6-wi-ngb" secondAttribute="height" multiplier="1:1" id="vt3-Qt-m21"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="2Ii-Hu-4ru">
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="2Ii-Hu-4ru">
|
||||
<rect key="frame" x="76" y="14" width="172" height="37"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Short" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qmI-m4-Mra">
|
||||
<rect key="frame" x="0.0" y="0.0" width="172" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="9Zk-Mp-JI7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="89.5" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="Short" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qmI-m4-Mra">
|
||||
<rect key="frame" x="0.0" y="0.0" width="42.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="4LS-dp-4VA">
|
||||
<rect key="frame" x="48.5" y="0.0" width="41" height="20.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xaB-Kc-Par">
|
||||
<rect key="frame" x="0.0" y="22.5" width="172" height="14.5"/>
|
||||
<rect key="frame" x="0.0" y="22.5" width="57.5" height="14.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -113,6 +121,7 @@
|
||||
<viewLayoutGuide key="safeArea" id="C6r-zO-INg"/>
|
||||
<connections>
|
||||
<outlet property="appIconImageView" destination="jg6-wi-ngb" id="j83-Dl-GT6"/>
|
||||
<outlet property="betaBadgeView" destination="4LS-dp-4VA" id="Q2Z-AG-Y19"/>
|
||||
<outlet property="dateLabel" destination="xaB-Kc-Par" id="mfG-3C-r7j"/>
|
||||
<outlet property="nameLabel" destination="qmI-m4-Mra" id="LQz-w7-HNb"/>
|
||||
<outlet property="updateButton" destination="OSL-U2-BKa" id="WbI-96-Nel"/>
|
||||
@@ -122,4 +131,7 @@
|
||||
<point key="canvasLocation" x="618.39999999999998" y="96.251874062968525"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="BetaBadge" width="41" height="17"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
27
AltStore/Patreon/Benefit.swift
Normal file
27
AltStore/Patreon/Benefit.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Benefit.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/21/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct BenefitResponse: Decodable
|
||||
{
|
||||
var id: String
|
||||
}
|
||||
}
|
||||
|
||||
struct Benefit: Hashable
|
||||
{
|
||||
var type: ALTPatreonBenefitType
|
||||
|
||||
init(response: PatreonAPI.BenefitResponse)
|
||||
{
|
||||
self.type = ALTPatreonBenefitType(response.id)
|
||||
}
|
||||
}
|
||||
27
AltStore/Patreon/Campaign.swift
Normal file
27
AltStore/Patreon/Campaign.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Campaign.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/21/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct CampaignResponse: Decodable
|
||||
{
|
||||
var id: String
|
||||
}
|
||||
}
|
||||
|
||||
struct Campaign
|
||||
{
|
||||
var identifier: String
|
||||
|
||||
init(response: PatreonAPI.CampaignResponse)
|
||||
{
|
||||
self.identifier = response.id
|
||||
}
|
||||
}
|
||||
380
AltStore/Patreon/PatreonAPI.swift
Normal file
380
AltStore/Patreon/PatreonAPI.swift
Normal file
@@ -0,0 +1,380 @@
|
||||
//
|
||||
// PatreonAPI.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AuthenticationServices
|
||||
|
||||
private let clientID = "ZMx0EGUWe4TVWYXNZZwK_fbIK5jHFVWoUf1Qb-sqNXmT-YzAGwDPxxq7ak3_W5Q2"
|
||||
private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt"
|
||||
private let creatorAccessToken = "mBh0yyK40Ibjzwb_cYeKIuzq8nNFBdEIlNPfgAQlhcU"
|
||||
|
||||
private let campaignID = "2863968"
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
enum Error: LocalizedError
|
||||
{
|
||||
case unknown
|
||||
case notAuthenticated
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .unknown: return NSLocalizedString("An unknown error occurred.", comment: "")
|
||||
case .notAuthenticated: return NSLocalizedString("No connected Patreon account.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PatreonAPI
|
||||
{
|
||||
static let shared = PatreonAPI()
|
||||
|
||||
var isAuthenticated: Bool {
|
||||
return Keychain.shared.patreonAccessToken != nil
|
||||
}
|
||||
|
||||
private var authenticationSession: ASWebAuthenticationSession?
|
||||
|
||||
private let session = URLSession(configuration: .ephemeral)
|
||||
private let baseURL = URL(string: "https://www.patreon.com/")!
|
||||
|
||||
private init()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
func authenticate(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
|
||||
{
|
||||
var components = URLComponents(string: "/oauth2/authorize")!
|
||||
components.queryItems = [URLQueryItem(name: "response_type", value: "code"),
|
||||
URLQueryItem(name: "client_id", value: clientID),
|
||||
URLQueryItem(name: "redirect_uri", value: "https://rileytestut.com/patreon/altstore")]
|
||||
|
||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
||||
|
||||
self.authenticationSession = ASWebAuthenticationSession(url: requestURL, callbackURLScheme: "altstore") { (callbackURL, error) in
|
||||
do
|
||||
{
|
||||
let callbackURL = try Result(callbackURL, error).get()
|
||||
|
||||
guard
|
||||
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
|
||||
let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }),
|
||||
let code = codeQueryItem.value
|
||||
else { throw Error.unknown }
|
||||
|
||||
self.fetchAccessToken(oauthCode: code) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success(let accessToken, let refreshToken):
|
||||
Keychain.shared.patreonAccessToken = accessToken
|
||||
Keychain.shared.patreonRefreshToken = refreshToken
|
||||
|
||||
self.fetchAccount(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
self.authenticationSession?.start()
|
||||
}
|
||||
|
||||
func fetchAccount(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
|
||||
{
|
||||
var components = URLComponents(string: "/api/oauth2/v2/identity")!
|
||||
components.queryItems = [URLQueryItem(name: "include", value: "memberships"),
|
||||
URLQueryItem(name: "fields[user]", value: "first_name,full_name"),
|
||||
URLQueryItem(name: "fields[member]", value: "full_name,patron_status")]
|
||||
|
||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
||||
let request = URLRequest(url: requestURL)
|
||||
|
||||
self.send(request, authorizationType: .user) { (result: Result<AccountResponse, Swift.Error>) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success(let response):
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let account = PatreonAccount(response: response, context: context)
|
||||
completion(.success(account))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPatrons(completion: @escaping (Result<[Patron], Swift.Error>) -> Void)
|
||||
{
|
||||
var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(campaignID)/members")!
|
||||
components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"),
|
||||
URLQueryItem(name: "fields[tier]", value: "title"),
|
||||
URLQueryItem(name: "fields[member]", value: "full_name,patron_status")]
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
return patron
|
||||
}.filter { $0.benefits.contains(where: { $0.type == .credits }) }
|
||||
|
||||
allPatrons.append(contentsOf: patrons)
|
||||
|
||||
if let nextURL = response.links?["next"]
|
||||
{
|
||||
fetchPatrons(url: nextURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(.success(allPatrons))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchPatrons(url: requestURL)
|
||||
}
|
||||
|
||||
func signOut(completion: @escaping (Result<Void, Swift.Error>) -> Void)
|
||||
{
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let accounts = PatreonAccount.all(in: context)
|
||||
accounts.forEach(context.delete(_:))
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
|
||||
Keychain.shared.patreonAccessToken = nil
|
||||
Keychain.shared.patreonRefreshToken = nil
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
func refreshPatreonAccount()
|
||||
{
|
||||
guard PatreonAPI.shared.isAuthenticated else { return }
|
||||
|
||||
PatreonAPI.shared.fetchAccount { (result: Result<PatreonAccount, Swift.Error>) in
|
||||
do
|
||||
{
|
||||
let account = try result.get()
|
||||
try account.managedObjectContext?.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to fetch Patreon account.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PatreonAPI
|
||||
{
|
||||
func fetchAccessToken(oauthCode: String, completion: @escaping (Result<(String, String), Swift.Error>) -> Void)
|
||||
{
|
||||
let encodedRedirectURI = ("https://rileytestut.com/patreon/altstore" as NSString).addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
|
||||
let encodedOauthCode = (oauthCode as NSString).addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
|
||||
|
||||
let body = "code=\(encodedOauthCode)&grant_type=authorization_code&client_id=\(clientID)&client_secret=\(clientSecret)&redirect_uri=\(encodedRedirectURI)"
|
||||
|
||||
let requestURL = URL(string: "/api/oauth2/token", relativeTo: self.baseURL)!
|
||||
|
||||
var request = URLRequest(url: requestURL)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = body.data(using: .utf8)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
struct Response: Decodable
|
||||
{
|
||||
var access_token: String
|
||||
var refresh_token: String
|
||||
}
|
||||
|
||||
self.send(request, authorizationType: .none) { (result: Result<Response, Swift.Error>) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success(let response): completion(.success((response.access_token, response.refresh_token)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshAccessToken(completion: @escaping (Result<Void, Swift.Error>) -> Void)
|
||||
{
|
||||
guard let refreshToken = Keychain.shared.patreonRefreshToken else { return }
|
||||
|
||||
var components = URLComponents(string: "/api/oauth2/token")!
|
||||
components.queryItems = [URLQueryItem(name: "grant_type", value: "refresh_token"),
|
||||
URLQueryItem(name: "refresh_token", value: refreshToken),
|
||||
URLQueryItem(name: "client_id", value: clientID),
|
||||
URLQueryItem(name: "client_secret", value: clientSecret)]
|
||||
|
||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
||||
|
||||
var request = URLRequest(url: requestURL)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
struct Response: Decodable
|
||||
{
|
||||
var access_token: String
|
||||
var refresh_token: String
|
||||
}
|
||||
|
||||
self.send(request, authorizationType: .none) { (result: Result<Response, Swift.Error>) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success(let response):
|
||||
Keychain.shared.patreonAccessToken = response.access_token
|
||||
Keychain.shared.patreonRefreshToken = response.refresh_token
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send<ResponseType: Decodable>(_ request: URLRequest, authorizationType: AuthorizationType, completion: @escaping (Result<ResponseType, Swift.Error>) -> Void)
|
||||
{
|
||||
var request = request
|
||||
|
||||
switch authorizationType
|
||||
{
|
||||
case .none: break
|
||||
case .creator:
|
||||
request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization")
|
||||
case .user:
|
||||
guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(Error.notAuthenticated)) }
|
||||
request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
let task = self.session.dataTask(with: request) { (data, response, error) in
|
||||
do
|
||||
{
|
||||
let data = try Result(data, error).get()
|
||||
|
||||
if let response = response as? HTTPURLResponse, response.statusCode == 401
|
||||
{
|
||||
if authorizationType == .user
|
||||
{
|
||||
self.refreshAccessToken() { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success: self.send(request, authorizationType: authorizationType, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(.failure(Error.notAuthenticated))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let response = try JSONDecoder().decode(ResponseType.self, from: data)
|
||||
completion(.success(response))
|
||||
}
|
||||
catch let error
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
69
AltStore/Patreon/Patron.swift
Normal file
69
AltStore/Patreon/Patron.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// Patron.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/21/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct PatronResponse: 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?
|
||||
}
|
||||
}
|
||||
|
||||
extension Patron
|
||||
{
|
||||
enum Status: String, Decodable
|
||||
{
|
||||
case active = "active_patron"
|
||||
case declined = "declined_patron"
|
||||
case former = "former_patron"
|
||||
}
|
||||
}
|
||||
|
||||
class Patron
|
||||
{
|
||||
var name: String
|
||||
var identifier: String
|
||||
|
||||
var status: Status
|
||||
|
||||
var benefits: Set<Benefit> = []
|
||||
|
||||
init(response: PatreonAPI.PatronResponse)
|
||||
{
|
||||
self.name = response.attributes.full_name
|
||||
self.identifier = response.id
|
||||
self.status = Status(rawValue: response.attributes.patron_status) ?? .former
|
||||
}
|
||||
}
|
||||
50
AltStore/Patreon/Tier.swift
Normal file
50
AltStore/Patreon/Tier.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Tier.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/21/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct TierResponse: 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
|
||||
}
|
||||
}
|
||||
|
||||
struct Tier
|
||||
{
|
||||
var name: String
|
||||
var identifier: String
|
||||
|
||||
var benefits: [Benefit] = []
|
||||
|
||||
init(response: PatreonAPI.TierResponse)
|
||||
{
|
||||
self.name = response.attributes.title
|
||||
self.identifier = response.id
|
||||
self.benefits = response.relationships.benefits.data.map(Benefit.init(response:))
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,29 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AltStore",
|
||||
"bundleIdentifier": "com.rileytestut.AltStore.Beta",
|
||||
"developerName": "Riley Testut",
|
||||
"version": "0.3",
|
||||
"versionDate": "2019-09-01",
|
||||
"versionDescription": "AltStore beta includes additional features, such as sideloading apps.",
|
||||
"downloadURL": "https://www.dropbox.com/s/25otlzsch7ubkyu/AltStore-Beta.ipa?dl=1",
|
||||
"localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png",
|
||||
"size": 10010524,
|
||||
"beta": true,
|
||||
"permissions": [
|
||||
{
|
||||
"type": "background-fetch",
|
||||
"usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring."
|
||||
},
|
||||
{
|
||||
"type": "background-audio",
|
||||
"usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Delta",
|
||||
"bundleIdentifier": "com.rileytestut.Delta",
|
||||
|
||||
BIN
AltStore/Resources/Assets.xcassets/BetaBadge.imageset/BETA.png
vendored
Normal file
BIN
AltStore/Resources/Assets.xcassets/BetaBadge.imageset/BETA.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
AltStore/Resources/Assets.xcassets/BetaBadge.imageset/BETA@2x.png
vendored
Normal file
BIN
AltStore/Resources/Assets.xcassets/BetaBadge.imageset/BETA@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
AltStore/Resources/Assets.xcassets/BetaBadge.imageset/BETA@3x.png
vendored
Normal file
BIN
AltStore/Resources/Assets.xcassets/BetaBadge.imageset/BETA@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
23
AltStore/Resources/Assets.xcassets/BetaBadge.imageset/Contents.json
vendored
Normal file
23
AltStore/Resources/Assets.xcassets/BetaBadge.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BETA.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BETA@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BETA@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
129
AltStore/Settings/PatreonViewController.swift
Normal file
129
AltStore/Settings/PatreonViewController.swift
Normal file
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// PatreonViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AuthenticationServices
|
||||
|
||||
import Roxas
|
||||
|
||||
class PatreonViewController: UITableViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
@IBOutlet private var signInButton: UIBarButtonItem!
|
||||
@IBOutlet private var signOutButton: UIBarButtonItem!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.fetchPatrons()
|
||||
}
|
||||
}
|
||||
|
||||
private extension PatreonViewController
|
||||
{
|
||||
func makeDataSource() -> RSTArrayTableViewDataSource<Patron>
|
||||
{
|
||||
let dataSource = RSTArrayTableViewDataSource<Patron>(items: [])
|
||||
dataSource.cellConfigurationHandler = { (cell, patron, indexPath) in
|
||||
cell.textLabel?.text = patron.name
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
if PatreonAPI.shared.isAuthenticated && DatabaseManager.shared.patreonAccount() != nil
|
||||
{
|
||||
self.navigationItem.rightBarButtonItem = self.signOutButton
|
||||
}
|
||||
else
|
||||
{
|
||||
self.navigationItem.rightBarButtonItem = self.signInButton
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPatrons()
|
||||
{
|
||||
PatreonAPI.shared.fetchPatrons { (result) in
|
||||
do
|
||||
{
|
||||
let patrons = try result.get()
|
||||
self.dataSource.items = patrons
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PatreonViewController
|
||||
{
|
||||
@IBAction func authenticate(_ sender: UIBarButtonItem)
|
||||
{
|
||||
PatreonAPI.shared.authenticate { (result) in
|
||||
do
|
||||
{
|
||||
let account = try result.get()
|
||||
try account.managedObjectContext?.save()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
catch ASWebAuthenticationSessionError.canceledLogin
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func signOut(_ sender: UIBarButtonItem)
|
||||
{
|
||||
PatreonAPI.shared.signOut { (result) in
|
||||
do
|
||||
{
|
||||
try result.get()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
AltStore/Types/ALTPatreonBenefitType.h
Normal file
13
AltStore/Types/ALTPatreonBenefitType.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 *ALTPatreonBenefitType NS_TYPED_EXTENSIBLE_ENUM;
|
||||
extern ALTPatreonBenefitType const ALTPatreonBenefitTypeBetaAccess;
|
||||
extern ALTPatreonBenefitType const ALTPatreonBenefitTypeCredits;
|
||||
12
AltStore/Types/ALTPatreonBenefitType.m
Normal file
12
AltStore/Types/ALTPatreonBenefitType.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 "ALTPatreonBenefitType.h"
|
||||
|
||||
ALTPatreonBenefitType const ALTPatreonBenefitTypeBetaAccess = @"1186336";
|
||||
ALTPatreonBenefitType const ALTPatreonBenefitTypeCredits = @"1186340";
|
||||
Reference in New Issue
Block a user