Improves App ID counting + management

Fetches App ID count directly from Apple, and adds AppIDsViewController to view all App IDs for the logged-in account.
This commit is contained in:
Riley Testut
2020-02-10 17:30:11 -08:00
parent 390a770115
commit 5045c1057a
14 changed files with 951 additions and 102 deletions

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
@@ -116,6 +116,9 @@
BF4C7F2523801F0800B2556E /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9B63C5229DD44D002F0A62 /* AltSign.framework */; };
BF4C7F27238086EB00B2556E /* InstallPlugin.sh in Resources */ = {isa = PBXBuildFile; fileRef = BF4C7F26238086EB00B2556E /* InstallPlugin.sh */; };
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */; };
BF56D2AA23DF88310006506D /* AppID.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2A923DF88310006506D /* AppID.swift */; };
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */; };
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */; };
BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; };
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */; };
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */; };
@@ -225,6 +228,8 @@
BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; };
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; };
BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */; };
BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE944023F22AA100CDA07D /* AppIDComponents.swift */; };
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; };
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; };
BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */; };
@@ -418,6 +423,10 @@
BF4C7F26238086EB00B2556E /* InstallPlugin.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = InstallPlugin.sh; sourceTree = "<group>"; };
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>"; };
BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 4.xcdatamodel"; sourceTree = "<group>"; };
BF56D2A923DF88310006506D /* AppID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppID.swift; sourceTree = "<group>"; };
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppIDsOperation.swift; sourceTree = "<group>"; };
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDsViewController.swift; sourceTree = "<group>"; };
BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AltPlugin.mailbundle; sourceTree = BUILT_PRODUCTS_DIR; };
BF5C5FC7237DF5AE00EDD0C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -545,6 +554,8 @@
BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = "<group>"; };
BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = "<group>"; };
BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTApplication+AppExtensions.swift"; sourceTree = "<group>"; };
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDComponents.swift; sourceTree = "<group>"; };
BFF0B68D23219520007A79E1 /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = "<group>"; };
BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonComponents.swift; sourceTree = "<group>"; };
BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AboutPatreonHeaderView.xib; sourceTree = "<group>"; };
@@ -846,6 +857,15 @@
name = libcnary;
sourceTree = "<group>";
};
BF56D2AD23DF9E170006506D /* App IDs */ = {
isa = PBXGroup;
children = (
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */,
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */,
);
path = "App IDs";
sourceTree = "<group>";
};
BF5C5FC6237DF5AE00EDD0C6 /* AltPlugin */ = {
isa = PBXGroup;
children = (
@@ -973,6 +993,7 @@
BFDB69FB22A9A7A6007EA6D6 /* Settings */,
BFD5D6E6230CC94B007955AB /* Patreon */,
BFD2478A2284C49000981D42 /* Managing Apps */,
BF56D2AD23DF9E170006506D /* App IDs */,
BFC51D7922972F1F00388324 /* Server */,
BFD247982284D7FC00981D42 /* Model */,
BFDB6A0922AAEDA1007EA6D6 /* Operations */,
@@ -1056,6 +1077,7 @@
BFB11691229322E400BB457C /* DatabaseManager.swift */,
BF3D64A122E8031100E9056B /* MergePolicy.swift */,
BFE6326722A858F300F30809 /* Account.swift */,
BF56D2A923DF88310006506D /* AppID.swift */,
BF3D648722E79A3700E9056B /* AppPermission.swift */,
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
BFA8172C23C5823E001B5953 /* InstalledExtension.swift */,
@@ -1079,6 +1101,7 @@
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */,
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */,
BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */,
BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -1152,6 +1175,7 @@
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
);
path = Operations;
sourceTree = "<group>";
@@ -1652,6 +1676,8 @@
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */,
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */,
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */,
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */,
@@ -1676,6 +1702,7 @@
BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */,
BFE6326822A858F300F30809 /* Account.swift in Sources */,
BFE6326622A857C200F30809 /* Team.swift in Sources */,
BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
@@ -1684,6 +1711,7 @@
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */,
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
@@ -1730,6 +1758,7 @@
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
BF770E5622BC3C03002A40FE /* Server.swift in Sources */,
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */,
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */,
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2291,11 +2320,12 @@
BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */,
BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */,
BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */,
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */,
);
currentVersion = BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */;
currentVersion = BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */;
path = AltStore.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

View File

@@ -0,0 +1,30 @@
//
// AppIDComponents.swift
// AltStore
//
// Created by Riley Testut on 2/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
class AppIDCollectionViewCell: UICollectionViewCell
{
@IBOutlet var bannerView: AppBannerView!
override func awakeFromNib()
{
super.awakeFromNib()
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.contentView.preservesSuperviewLayoutMargins = true
self.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
self.bannerView.buttonLabel.isHidden = false
}
}
class AppIDsCollectionReusableView: UICollectionReusableView
{
@IBOutlet var textLabel: UILabel!
}

View File

@@ -0,0 +1,225 @@
//
// AppIDsViewController.swift
// AltStore
//
// Created by Riley Testut on 1/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class AppIDsViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
private var didInitialFetch = false
private var isLoading = false {
didSet {
self.update()
}
}
@IBOutlet var activityIndicatorBarButtonItem: UIBarButtonItem!
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView.dataSource = self.dataSource
self.activityIndicatorBarButtonItem.isIndicatingActivity = true
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(AppIDsViewController.fetchAppIDs), for: .primaryActionTriggered)
self.collectionView.refreshControl = refreshControl
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if !self.didInitialFetch
{
self.fetchAppIDs()
}
}
}
private extension AppIDsViewController
{
func makeDataSource() -> RSTFetchedResultsCollectionViewDataSource<AppID>
{
let fetchRequest = AppID.fetchRequest() as NSFetchRequest<AppID>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AppID.name, ascending: true),
NSSortDescriptor(keyPath: \AppID.bundleIdentifier, ascending: true),
NSSortDescriptor(keyPath: \AppID.expirationDate, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
if let team = DatabaseManager.shared.activeTeam()
{
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(AppID.team), team)
}
else
{
fetchRequest.predicate = NSPredicate(value: false)
}
let dataSource = RSTFetchedResultsCollectionViewDataSource<AppID>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.proxy = self
dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in
let tintColor = UIColor.altPrimary
let cell = cell as! AppIDCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = tintColor
cell.bannerView.iconImageView.isHidden = true
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.betaBadgeView.isHidden = true
if let expirationDate = appID.expirationDate
{
cell.bannerView.button.isHidden = false
cell.bannerView.button.isUserInteractionEnabled = false
cell.bannerView.buttonLabel.isHidden = false
let currentDate = Date()
let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate)
if numberOfDays == 1
{
cell.bannerView.button.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal)
}
else
{
cell.bannerView.button.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal)
}
}
else
{
cell.bannerView.button.isHidden = true
cell.bannerView.buttonLabel.isHidden = true
}
cell.bannerView.titleLabel.text = appID.name
cell.bannerView.subtitleLabel.text = appID.bundleIdentifier
cell.bannerView.subtitleLabel.numberOfLines = 2
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
}
return dataSource
}
@objc func fetchAppIDs()
{
guard !self.isLoading else { return }
self.isLoading = true
AppManager.shared.fetchAppIDs { (result) in
do
{
let (_, context) = try result.get()
try context.save()
}
catch
{
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
DispatchQueue.main.async {
self.isLoading = false
}
}
}
func update()
{
if !self.isLoading
{
self.collectionView.refreshControl?.endRefreshing()
self.activityIndicatorBarButtonItem.isIndicatingActivity = false
}
}
}
extension AppIDsViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: collectionView.bounds.width, height: 80)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
{
let indexPath = IndexPath(row: 0, section: section)
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
// Use this view to calculate the optimal size based on the collection view's width
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required, // Width is fixed
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
return size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
{
return CGSize(width: collectionView.bounds.width, height: 50)
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
switch kind
{
case UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! AppIDsCollectionReusableView
headerView.layoutMargins.left = self.view.layoutMargins.left
headerView.layoutMargins.right = self.view.layoutMargins.right
if let activeTeam = DatabaseManager.shared.activeTeam(), activeTeam.type == .free
{
headerView.textLabel.text = """
Each app and app extension installed with AltStore must register an App ID with Apple. Apple limits free developer accounts to 10 App IDs at a time.
App IDs expire after one week, but AltStore will automatically renew them for all installed apps. Once an App ID expires, it no longer counts toward your total.
"""
}
else
{
headerView.textLabel.text = """
Each app and app extension installed with AltStore must register an App ID with Apple.
App IDs for paid developer accounts never expire, and there is no limit to how many you can create.
"""
}
return headerView
case UICollectionView.elementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! AppIDsCollectionReusableView
let count = self.dataSource.itemCount
if count == 1
{
footerView.textLabel.text = NSLocalizedString("1 App ID", comment: "")
}
else
{
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs", comment: ""), NSNumber(value: count))
}
return footerView
default: fatalError()
}
}
}

View File

@@ -629,7 +629,7 @@ World</string>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="SB5-U0-jyy">
<size key="itemSize" width="375" height="60"/>
<size key="headerReferenceSize" width="50" height="50"/>
<size key="footerReferenceSize" width="50" height="50"/>
<size key="footerReferenceSize" width="50" height="60.5"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
@@ -751,32 +751,33 @@ World</string>
</connections>
</collectionReusableView>
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsFooter" id="HYs-co-nJZ" customClass="InstalledAppsCollectionFooterView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="185" width="375" height="50"/>
<rect key="frame" x="0.0" y="185" width="375" height="60.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="5/10 App IDs" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LLv-8I-6Of">
<rect key="frame" x="138.5" y="0.0" width="98" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NHb-0F-cHZ">
<rect key="frame" x="241.5" y="-0.5" width="22" height="22"/>
<constraints>
<constraint firstAttribute="width" constant="22" id="K1V-bV-Mjg"/>
<constraint firstAttribute="height" constant="22" id="vW1-aN-slG"/>
</constraints>
<state key="normal" image="questionmark.circle" catalog="system"/>
<connections>
<action selector="presentAppIDHelpAlert:" destination="hv7-Ar-voT" eventType="touchUpInside" id="m2N-Lx-adY"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="900" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="GFQ-Wy-Qhy">
<rect key="frame" x="138.5" y="0.0" width="98" height="52.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="5/10 App IDs" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LLv-8I-6Of">
<rect key="frame" x="0.0" y="0.0" width="98" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NHb-0F-cHZ">
<rect key="frame" x="0.0" y="20.5" width="98" height="32"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<state key="normal" title="View App IDs"/>
<connections>
<segue destination="IXk-qg-mFJ" kind="presentation" identifier="showAppIDs" id="yZB-Fh-cTL"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="NHb-0F-cHZ" firstAttribute="leading" secondItem="LLv-8I-6Of" secondAttribute="trailing" constant="5" id="Fpk-zD-6AK"/>
<constraint firstItem="LLv-8I-6Of" firstAttribute="top" secondItem="HYs-co-nJZ" secondAttribute="top" id="UVG-MJ-SPb"/>
<constraint firstItem="LLv-8I-6Of" firstAttribute="centerX" secondItem="HYs-co-nJZ" secondAttribute="centerX" id="Wc0-ul-McA"/>
<constraint firstItem="NHb-0F-cHZ" firstAttribute="centerY" secondItem="LLv-8I-6Of" secondAttribute="centerY" id="wSf-Lv-NC0"/>
<constraint firstAttribute="bottom" secondItem="GFQ-Wy-Qhy" secondAttribute="bottom" constant="8" id="HGl-P6-G2v"/>
<constraint firstItem="GFQ-Wy-Qhy" firstAttribute="top" secondItem="HYs-co-nJZ" secondAttribute="top" id="gg9-XU-2ej"/>
<constraint firstItem="GFQ-Wy-Qhy" firstAttribute="centerX" secondItem="HYs-co-nJZ" secondAttribute="centerX" id="vyo-h4-yD9"/>
</constraints>
<connections>
<outlet property="button" destination="NHb-0F-cHZ" id="wOh-Ee-jhN"/>
@@ -803,6 +804,114 @@ World</string>
</objects>
<point key="canvasLocation" x="1728.8" y="716.49175412293857"/>
</scene>
<!--App IDs-->
<scene sceneID="kvf-US-rRe">
<objects>
<collectionViewController title="App IDs" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="Wzt-qc-XG8">
<size key="itemSize" width="375" height="80"/>
<size key="headerReferenceSize" width="50" height="60"/>
<size key="footerReferenceSize" width="50" height="50"/>
<inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XWu-DU-xbh" customClass="AppIDCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="70" width="375" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
<accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/>
</accessibility>
</view>
</subviews>
</view>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="1w8-fI-98T" secondAttribute="trailing" id="0bS-49-dqo"/>
<constraint firstAttribute="bottom" secondItem="1w8-fI-98T" secondAttribute="bottom" id="Bif-xB-0gt"/>
<constraint firstItem="1w8-fI-98T" firstAttribute="top" secondItem="XWu-DU-xbh" secondAttribute="top" id="aEf-KK-MHU"/>
<constraint firstItem="1w8-fI-98T" firstAttribute="leading" secondItem="XWu-DU-xbh" secondAttribute="leadingMargin" id="mFW-ti-cVB"/>
</constraints>
<connections>
<outlet property="bannerView" destination="1w8-fI-98T" id="OH8-L9-TZn"/>
</connections>
</collectionViewCell>
</cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="AppIDsCollectionReusableView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="App IDs Description" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="83Z-Ih-nOW">
<rect key="frame" x="8" y="14" width="359" height="31"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="83Z-Ih-nOW" secondAttribute="bottom" constant="15" id="CQA-og-LZ2"/>
<constraint firstItem="83Z-Ih-nOW" firstAttribute="top" secondItem="th0-G6-bRt" secondAttribute="top" constant="14" id="e0J-MA-eH5"/>
<constraint firstAttribute="leadingMargin" secondItem="83Z-Ih-nOW" secondAttribute="leading" id="nGf-Rh-mnk"/>
<constraint firstAttribute="trailingMargin" secondItem="83Z-Ih-nOW" secondAttribute="trailing" id="sYg-nT-ror"/>
</constraints>
<connections>
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
</connections>
</collectionReusableView>
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="AppIDsCollectionReusableView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="170" width="375" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="10 App IDs" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Zna-7n-kBz">
<rect key="frame" x="146" y="0.0" width="83" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="Zna-7n-kBz" firstAttribute="centerX" secondItem="xMh-lD-r6C" secondAttribute="centerX" id="7RS-ua-XzZ"/>
<constraint firstItem="Zna-7n-kBz" firstAttribute="top" secondItem="xMh-lD-r6C" secondAttribute="top" id="RvY-z8-XI6"/>
</constraints>
<connections>
<outlet property="textLabel" destination="Zna-7n-kBz" id="LK5-BR-skx"/>
</connections>
</collectionReusableView>
<connections>
<outlet property="dataSource" destination="y1A-Nm-mw7" id="U8O-CF-Jhv"/>
<outlet property="delegate" destination="y1A-Nm-mw7" id="a8i-FA-aUq"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
<barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
<rect key="frame" x="16" y="7" width="83" height="42"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</view>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="Ekd-oC-gOr">
<connections>
<segue destination="eS1-sQ-VUA" kind="unwind" unwindAction="unwindToMyAppsViewController:" id="VHS-kt-woS"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="activityIndicatorBarButtonItem" destination="Aqs-QK-Ups" id="2I7-rT-muy"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Lvd-jC-AZO" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="eS1-sQ-VUA" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="3301.5999999999999" y="715.59220389805103"/>
</scene>
<!--News-->
<scene sceneID="BV8-6J-nIv">
<objects>
@@ -823,6 +932,24 @@ World</string>
</objects>
<point key="canvasLocation" x="962" y="-752"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="1Gj-mS-BaN">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="y1A-Nm-mw7" kind="relationship" relationship="rootViewController" id="ZYf-6x-9a0"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="3LN-mt-qWn" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2526" y="731"/>
</scene>
</scenes>
<resources>
<image name="Back" width="18" height="18"/>
@@ -830,7 +957,6 @@ World</string>
<image name="MyApps" width="20" height="20"/>
<image name="News" width="19" height="20"/>
<image name="Settings" width="20" height="20"/>
<image name="questionmark.circle" catalog="system" width="64" height="60"/>
<namedColor name="Background">
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
@@ -842,7 +968,7 @@ World</string>
</namedColor>
</resources>
<inferredMetricsTieBreakers>
<segue reference="dzt-2e-VM9"/>
<segue reference="cnd-KK-o60"/>
</inferredMetricsTieBreakers>
<color key="tintColor" name="Primary"/>
</document>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -45,7 +45,7 @@
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="caL-vN-Svn">
<rect key="frame" x="85" y="18" width="195" height="52"/>
<rect key="frame" x="85" y="18" width="190" height="52"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="500" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="1Es-pv-zwd">
<rect key="frame" x="0.0" y="0.0" width="167" height="34"/>
@@ -67,13 +67,13 @@
</subviews>
</stackView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fC4-1V-iMn">
<rect key="frame" x="0.0" y="36" width="195" height="16"/>
<rect key="frame" x="0.0" y="36" width="190" height="16"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="LQh-pN-ePC">
<rect key="frame" x="0.0" y="0.0" width="195" height="16"/>
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="750" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="oN5-vu-Dnw">
<rect key="frame" x="0.0" y="0.0" width="195" height="16"/>
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
<accessibility key="accessibilityConfiguration" label="Developer"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -94,20 +94,20 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3ox-Q2-Rnd" userLabel="Button Stack View">
<rect key="frame" x="291" y="28.5" width="72" height="31"/>
<rect key="frame" x="286" y="28.5" width="77" height="31"/>
<subviews>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Yd9-jw-faD">
<rect key="frame" x="0.0" y="0.0" width="72" height="0.0"/>
<rect key="frame" x="0.0" y="0.0" width="77" height="0.0"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="10"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="72" height="31"/>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="31" id="Zwh-yQ-GTu"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="72" id="eGc-Dk-QbL"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="77" id="eGc-Dk-QbL"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<state key="normal" title="FREE"/>
@@ -135,7 +135,7 @@
<resources>
<image name="BetaBadge" width="41" height="17"/>
<namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@@ -97,7 +97,8 @@ extension AppManager
#endif
}
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTSigner, ALTAppleAPISession), Error>) -> Void)
@discardableResult
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTSigner, ALTAppleAPISession), Error>) -> Void) -> OperationGroup
{
let group = OperationGroup()
@@ -113,10 +114,20 @@ extension AppManager
let authenticationOperation = AuthenticationOperation(group: group, presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): group.error = error
case .success(let signer, let session):
group.signer = signer
group.session = session
}
completionHandler(result)
}
authenticationOperation.addDependency(findServerOperation)
self.operationQueue.addOperation(authenticationOperation)
return group
}
}
@@ -144,6 +155,23 @@ extension AppManager
self.operationQueue.addOperation(fetchSourceOperation)
}
}
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
{
var group: OperationGroup!
group = self.authenticate(presentingViewController: nil) { (result) in
switch result
{
case .failure(let error):
completionHandler(.failure(error))
case .success:
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
fetchAppIDsOperation.resultHandler = completionHandler
self.operationQueue.addOperation(fetchAppIDsOperation)
}
}
}
}
extension AppManager
@@ -382,6 +410,22 @@ private extension AppManager
refreshAnisetteDataOperation.addDependency(downloadOperation)
}
/* Cache App IDs */
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
fetchAppIDsOperation.resultHandler = { (result) in
do
{
let (_, context) = try result.get()
try context.save()
}
catch
{
print("Failed to fetch App IDs.", error)
}
}
operations.forEach { fetchAppIDsOperation.addDependency($0) }
operations.append(fetchAppIDsOperation)
group.addOperations(operations)
return group

View File

@@ -44,6 +44,11 @@ class Account: NSManagedObject, Fetchable
{
super.init(entity: Account.entity(), insertInto: context)
self.update(account: account)
}
func update(account: ALTAccount)
{
self.appleID = account.appleID
self.identifier = account.identifier

View File

@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>AltStore 3.xcdatamodel</string>
<string>AltStore 4.xcdatamodel</string>
</dict>
</plist>

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<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"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<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="193"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
<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="120"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
</elements>
</model>

View File

@@ -0,0 +1,52 @@
//
// AppID.swift
// AltStore
//
// Created by Riley Testut on 1/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
@objc(AppID)
class AppID: NSManagedObject, Fetchable
{
/* Properties */
@NSManaged var name: String
@NSManaged var identifier: String
@NSManaged var bundleIdentifier: String
@NSManaged var features: [ALTFeature: Any]
@NSManaged var expirationDate: Date?
/* Relationships */
@NSManaged private(set) var team: Team?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
init(_ appID: ALTAppID, team: Team, context: NSManagedObjectContext)
{
super.init(entity: AppID.entity(), insertInto: context)
self.name = appID.name
self.identifier = appID.identifier
self.bundleIdentifier = appID.bundleIdentifier
self.features = appID.features
self.expirationDate = appID.expirationDate
self.team = team
}
}
extension AppID
{
@nonobjc class func fetchRequest() -> NSFetchRequest<AppID>
{
return NSFetchRequest<AppID>(entityName: "AppID")
}
}

View File

@@ -43,6 +43,7 @@ class Team: NSManagedObject, Fetchable
/* Relationships */
@NSManaged private(set) var account: Account!
@NSManaged var installedApps: Set<InstalledApp>
@NSManaged private(set) var appIDs: Set<AppID>
var altTeam: ALTTeam?
@@ -55,13 +56,18 @@ class Team: NSManagedObject, Fetchable
{
super.init(entity: Team.entity(), insertInto: context)
self.account = account
self.update(team: team)
}
func update(team: ALTTeam)
{
self.altTeam = team
self.name = team.name
self.identifier = team.identifier
self.type = team.type
self.account = account
}
}

View File

@@ -113,6 +113,8 @@ class MyAppsViewController: UICollectionViewController
super.viewWillAppear(animated)
self.updateDataSource()
self.fetchAppIDs()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
@@ -142,6 +144,10 @@ class MyAppsViewController: UICollectionViewController
let installedApp = self.dataSource.item(at: indexPath)
return !installedApp.isSideloaded
}
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
{
}
}
private extension MyAppsViewController
@@ -373,6 +379,21 @@ private extension MyAppsViewController
}
}
func fetchAppIDs()
{
AppManager.shared.fetchAppIDs { (result) in
do
{
let (_, context) = try result.get()
try context.save()
}
catch
{
print("Failed to fetch App IDs.", error)
}
}
}
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping (Result<[String : Result<InstalledApp, Error>], Error>) -> Void)
{
func refresh()
@@ -700,19 +721,6 @@ private extension MyAppsViewController
self.present(alertController, animated: true, completion: nil)
}
@IBAction func presentAppIDHelpAlert(_ sender: UIButton)
{
let message = NSLocalizedString("""
Each app and app extension installed with AltStore must register an App ID with Apple. Apple limits free developer accounts to 10 App IDs at a time.
App IDs expire after one week, but AltStore will automatically renew them for all installed apps. Once an App ID expires, it no longer counts toward your total.
""", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("What are App IDs?", comment: ""), message: message, preferredStyle: .alert)
alertController.addAction(.ok)
self.present(alertController, animated: true, completion: nil)
}
}
private extension MyAppsViewController
@@ -843,24 +851,30 @@ extension MyAppsViewController
return headerView
case .installedApps:
let installedApps = self.installedAppsDataSource.fetchedResultsController.fetchedObjects ?? []
let registeredAppIDs = installedApps.filter { $0.team?.isActiveTeam ?? false }.reduce(0) { (sum, installedApp) in
// Each InstallApp has it's own app ID, plus one for each app extension.
return sum + 1 + installedApp.appExtensions.count
}
let maximumAppIDCount = 10
let remainingAppIDs = max(maximumAppIDCount - registeredAppIDs, 0)
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "InstalledAppsFooter", for: indexPath) as! InstalledAppsCollectionFooterView
if remainingAppIDs == 1
guard let team = DatabaseManager.shared.activeTeam() else { return footerView }
switch team.type
{
footerView.textLabel.text = String(format: NSLocalizedString("1 App ID Remaining", comment: ""))
}
else
{
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs))
case .free:
let registeredAppIDs = team.appIDs.count
let maximumAppIDCount = 10
let remainingAppIDs = max(maximumAppIDCount - registeredAppIDs, 0)
if remainingAppIDs == 1
{
footerView.textLabel.text = String(format: NSLocalizedString("1 App ID Remaining", comment: ""))
}
else
{
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs))
}
footerView.textLabel.isHidden = false
case .individual, .organization, .unknown: footerView.textLabel.isHidden = true
@unknown default: break
}
return footerView
@@ -941,14 +955,15 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
case .updates: return .zero
case .installedApps:
#if BETA
if let team = DatabaseManager.shared.activeTeam(), team.type == .free
{
return CGSize(width: collectionView.bounds.width, height: 44)
}
else
{
return .zero
}
guard let _ = DatabaseManager.shared.activeTeam() else { return .zero }
let indexPath = IndexPath(row: 0, section: section.rawValue)
let footerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionFooter, at: indexPath) as! InstalledAppsCollectionFooterView
let size = footerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
return size
#else
return .zero
#endif

View File

@@ -52,9 +52,6 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
private var appleIDPassword: String?
private var shouldShowInstructions = false
private var signer: ALTSigner?
private var session: ALTAppleAPISession?
private let operationQueue = OperationQueue()
private var submitCodeAction: UIAlertAction?
@@ -88,7 +85,6 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
{
case .failure(let error): self.finish(.failure(error))
case .success(let account, let session):
self.session = session
self.progress.completedUnitCount += 1
// Fetch Team
@@ -111,11 +107,22 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
case .success(let certificate):
self.progress.completedUnitCount += 1
let signer = ALTSigner(team: team, certificate: certificate)
self.signer = signer
// Save account/team to disk.
self.save(team) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
self.showInstructionsIfNecessary() { (didShowInstructions) in
self.finish(.success((signer, session)))
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
let signer = ALTSigner(team: team, certificate: certificate)
// Must cache App IDs _after_ saving account/team to disk.
self.cacheAppIDs(signer: signer, session: session) { (result) in
let result = result.map { _ in (signer, session) }
self.finish(result)
}
}
}
}
}
@@ -125,6 +132,47 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
}
}
func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
do
{
let account: Account
let team: Team
if let tempAccount = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context)
{
account = tempAccount
}
else
{
account = Account(altTeam.account, context: context)
}
if let tempTeam = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
{
team = tempTeam
}
else
{
team = Team(altTeam, account: account, context: context)
}
account.update(account: altTeam.account)
team.update(team: altTeam)
try context.save()
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
}
override func finish(_ result: Result<(ALTSigner, ALTAppleAPISession), Error>)
{
guard !self.isFinished else { return }
@@ -132,14 +180,17 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
print("Finished authenticating with result:", result)
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
context.perform {
do
{
let (signer, session) = try result.get()
let altAccount = signer.team.account
guard
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), signer.team.account.identifier), in: context),
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), signer.team.identifier), in: context)
else { throw AuthenticationError.noTeam }
// Account
let account = Account(altAccount, context: context)
account.isActiveAccount = true
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
@@ -152,7 +203,6 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
}
// Team
let team = Team(signer.team, account: account, context: context)
team.isActiveTeam = true
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
@@ -174,24 +224,27 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
try context.save()
// Update keychain
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
Keychain.shared.appleIDEmailAddress = signer.team.account.appleID
Keychain.shared.appleIDPassword = self.appleIDPassword
Keychain.shared.signingCertificate = signer.certificate.p12Data()
Keychain.shared.signingCertificatePassword = signer.certificate.machineIdentifier
// Refresh screen must go last since a successful refresh will cause the app to quit.
self.showRefreshScreenIfNecessary() { (didShowRefreshAlert) in
super.finish(.success((signer, session)))
self.showInstructionsIfNecessary() { (didShowInstructions) in
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
// Refresh screen must go last since a successful refresh will cause the app to quit.
self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in
super.finish(result)
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
}
}
}
}
catch
{
super.finish(.failure(error))
super.finish(result)
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
@@ -504,6 +557,30 @@ private extension AuthenticationOperation
}
}
func cacheAppIDs(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let group = OperationGroup()
group.signer = signer
group.session = session
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
fetchAppIDsOperation.resultHandler = { (result) in
do
{
let (_, context) = try result.get()
try context.save()
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
self.operationQueue.addOperation(fetchAppIDsOperation)
}
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
{
guard self.shouldShowInstructions else { return completionHandler(false) }
@@ -522,9 +599,8 @@ private extension AuthenticationOperation
}
}
func showRefreshScreenIfNecessary(completionHandler: @escaping (Bool) -> Void)
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
{
guard let signer = self.signer, let session = self.session else { return completionHandler(false) }
guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) }
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.

View File

@@ -0,0 +1,73 @@
//
// FetchAppIDsOperation.swift
// AltStore
//
// Created by Riley Testut on 1/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
import AltKit
import Roxas
@objc(FetchAppIDsOperation)
class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
{
let group: OperationGroup
let context: NSManagedObjectContext
init(group: OperationGroup, context: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
{
self.group = group
self.context = context
super.init()
}
override func main()
{
super.main()
if let error = self.group.error
{
self.finish(.failure(error))
return
}
guard
let team = self.group.signer?.team,
let session = self.group.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
self.context.perform {
do
{
let fetchedAppIDs = try Result(appIDs, error).get()
guard let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), team.identifier), in: self.context) else { throw OperationError.notAuthenticated }
let fetchedIdentifiers = fetchedAppIDs.map { $0.identifier }
let deletedAppIDsRequest = AppID.fetchRequest() as NSFetchRequest<AppID>
deletedAppIDsRequest.predicate = NSPredicate(format: "%K == %@ AND NOT (%K IN %@)",
#keyPath(AppID.team), team,
#keyPath(AppID.identifier), fetchedIdentifiers)
let deletedAppIDs = try self.context.fetch(deletedAppIDsRequest)
deletedAppIDs.forEach { self.context.delete($0) }
let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.context) }
self.finish(.success((appIDs, self.context)))
}
catch
{
self.finish(.failure(error))
}
}
}
}
}