diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index b45cca96..bcfbc7b2 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -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 = ""; }; BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = ""; }; BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = ""; }; + BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 4.xcdatamodel"; sourceTree = ""; }; + BF56D2A923DF88310006506D /* AppID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppID.swift; sourceTree = ""; }; + BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppIDsOperation.swift; sourceTree = ""; }; + BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDsViewController.swift; sourceTree = ""; }; 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 = ""; }; @@ -545,6 +554,8 @@ BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = ""; }; BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = ""; }; + BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTApplication+AppExtensions.swift"; sourceTree = ""; }; + BFEE944023F22AA100CDA07D /* AppIDComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDComponents.swift; sourceTree = ""; }; BFF0B68D23219520007A79E1 /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = ""; }; BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonComponents.swift; sourceTree = ""; }; BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AboutPatreonHeaderView.xib; sourceTree = ""; }; @@ -846,6 +857,15 @@ name = libcnary; sourceTree = ""; }; + BF56D2AD23DF9E170006506D /* App IDs */ = { + isa = PBXGroup; + children = ( + BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */, + BFEE944023F22AA100CDA07D /* AppIDComponents.swift */, + ); + path = "App IDs"; + sourceTree = ""; + }; 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 = ""; @@ -1152,6 +1175,7 @@ BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */, BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */, BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */, + BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */, ); path = Operations; sourceTree = ""; @@ -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 = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/AltStore/App IDs/AppIDComponents.swift b/AltStore/App IDs/AppIDComponents.swift new file mode 100644 index 00000000..abbfca0d --- /dev/null +++ b/AltStore/App IDs/AppIDComponents.swift @@ -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! +} diff --git a/AltStore/App IDs/AppIDsViewController.swift b/AltStore/App IDs/AppIDsViewController.swift new file mode 100644 index 00000000..b029e0d2 --- /dev/null +++ b/AltStore/App IDs/AppIDsViewController.swift @@ -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 + { + let fetchRequest = AppID.fetchRequest() as NSFetchRequest + 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(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() + } + } +} diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index 8886f81d..99fa18d2 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -629,7 +629,7 @@ World - + @@ -751,32 +751,33 @@ World - + - - + + + + + + + - - - - + + + @@ -803,6 +804,114 @@ World + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -823,6 +932,24 @@ World + + + + + + + + + + + + + + + + + + @@ -830,7 +957,6 @@ World - @@ -842,7 +968,7 @@ World - + diff --git a/AltStore/Components/AppBannerView.xib b/AltStore/Components/AppBannerView.xib index cb5810f4..7c6634f6 100644 --- a/AltStore/Components/AppBannerView.xib +++ b/AltStore/Components/AppBannerView.xib @@ -1,9 +1,9 @@ - + - + @@ -45,7 +45,7 @@ - + @@ -67,13 +67,13 @@ - + - + - + -