diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift
index 6b4fbd4c..65dc2c40 100644
--- a/AltStore/AppDelegate.swift
+++ b/AltStore/AppDelegate.swift
@@ -52,7 +52,10 @@ private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, Uns
extension AppDelegate
{
- static let openPatreonSettingsDeepLinkNotification = Notification.Name("openPatreonSettingsDeepLinkNotification")
+ static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
+ static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
+
+ static let importAppDeepLinkURLKey = "fileURL"
}
@UIApplicationMain
@@ -115,14 +118,27 @@ private extension AppDelegate
func open(_ url: URL) -> Bool
{
- guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
- guard let host = components.host, host.lowercased() == "patreon" else { return false }
-
- DispatchQueue.main.async {
- NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
+ if url.isFileURL
+ {
+ guard url.pathExtension.lowercased() == "ipa" else { return false }
+
+ DispatchQueue.main.async {
+ NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
+ }
+
+ return true
+ }
+ else
+ {
+ guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
+ guard let host = components.host, host.lowercased() == "patreon" else { return false }
+
+ DispatchQueue.main.async {
+ NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
+ }
+
+ return true
}
-
- return true
}
}
diff --git a/AltStore/Info.plist b/AltStore/Info.plist
index c53d860a..61de066e 100644
--- a/AltStore/Info.plist
+++ b/AltStore/Info.plist
@@ -8,6 +8,23 @@
1AAAB6FD-E8CE-4B70-8F26-4073215C03B0
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
+ CFBundleDocumentTypes
+
+
+ CFBundleTypeIconFiles
+
+ CFBundleTypeName
+ iOS App
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+ LSItemContentTypes
+
+ com.apple.itunes.ipa
+
+
+
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift
index 813e3af8..605efa85 100644
--- a/AltStore/My Apps/MyAppsViewController.swift
+++ b/AltStore/My Apps/MyAppsViewController.swift
@@ -60,6 +60,7 @@ class MyAppsViewController: UICollectionViewController
super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(MyAppsViewController.didFetchSource(_:)), name: AppManager.didFetchSourceNotification, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(MyAppsViewController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
}
override func viewDidLoad()
@@ -577,23 +578,83 @@ private extension MyAppsViewController
@IBAction func sideloadApp(_ sender: UIBarButtonItem)
{
- func sideloadApp()
- {
+ self.presentSideloadingAlert { (shouldContinue) in
+ guard shouldContinue else { return }
+
let iOSAppUTI = "com.apple.itunes.ipa" // Declared by the system.
let documentPickerViewController = UIDocumentPickerViewController(documentTypes: [iOSAppUTI], in: .import)
documentPickerViewController.delegate = self
self.present(documentPickerViewController, animated: true, completion: nil)
}
-
+ }
+
+ func presentSideloadingAlert(completion: @escaping (Bool) -> Void)
+ {
let alertController = UIAlertController(title: NSLocalizedString("Sideload Apps (Beta)", comment: ""), message: NSLocalizedString("You may only install 10 apps + app extensions per week due to Apple's restrictions.\n\nIf you encounter an app that is not able to be sideloaded, please report the app to support@altstore.io.", comment: ""), preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .default, handler: { (action) in
- sideloadApp()
+ completion(true)
+ }))
+ alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
+ completion(false)
}))
- alertController.addAction(.cancel)
self.present(alertController, animated: true, completion: nil)
}
+ func installApp(at fileURL: URL, completion: @escaping (Result) -> Void)
+ {
+ self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
+
+ DispatchQueue.global().async {
+ let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
+
+ do
+ {
+ try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
+
+ let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory)
+
+ guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { return }
+
+ self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in
+ try? FileManager.default.removeItem(at: temporaryDirectory)
+
+ DispatchQueue.main.async {
+ if let error = result.error
+ {
+ let toastView = ToastView(text: error.localizedDescription, detailText: nil)
+ toastView.show(in: self.view, duration: 2.0)
+ }
+ else
+ {
+ print("Successfully installed app:", application.bundleIdentifier)
+ }
+
+ self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
+ self.sideloadingProgressView.observedProgress = nil
+ self.sideloadingProgressView.setHidden(true, animated: true)
+
+ completion(.success(()))
+ }
+ }
+
+ DispatchQueue.main.async {
+ self.sideloadingProgressView.progress = 0
+ self.sideloadingProgressView.isHidden = false
+ self.sideloadingProgressView.observedProgress = self.sideloadingProgress
+ }
+ }
+ catch
+ {
+ try? FileManager.default.removeItem(at: temporaryDirectory)
+
+ self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
+
+ completion(.failure(error))
+ }
+ }
+ }
+
@objc func presentAlert(for installedApp: InstalledApp)
{
let alertController = UIAlertController(title: nil, message: NSLocalizedString("Removing a sideloaded app only removes it from AltStore. You must also delete it from the home screen to fully uninstall the app.", comment: ""), preferredStyle: .actionSheet)
@@ -643,6 +704,41 @@ private extension MyAppsViewController
self.presentAlert(for: installedApp)
}
+
+ @objc func importApp(_ notification: Notification)
+ {
+ #if BETA
+
+ guard let fileURL = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return }
+ guard self.presentedViewController == nil else { return }
+
+ func finish()
+ {
+ do
+ {
+ try FileManager.default.removeItem(at: fileURL)
+ }
+ catch
+ {
+ print("Unable to remove imported .ipa.", error)
+ }
+ }
+
+ self.presentSideloadingAlert { (shouldContinue) in
+ if shouldContinue
+ {
+ self.installApp(at: fileURL) { (result) in
+ finish()
+ }
+ }
+ else
+ {
+ finish()
+ }
+ }
+
+ #endif
+ }
}
extension MyAppsViewController
@@ -834,51 +930,8 @@ extension MyAppsViewController: UIDocumentPickerDelegate
{
guard let fileURL = urls.first else { return }
- self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
-
- DispatchQueue.global().async {
- let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
-
- do
- {
- try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
-
- let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory)
-
- guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { return }
-
- self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in
- try? FileManager.default.removeItem(at: temporaryDirectory)
-
- DispatchQueue.main.async {
- if let error = result.error
- {
- let toastView = ToastView(text: error.localizedDescription, detailText: nil)
- toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
- }
- else
- {
- print("Successfully installed app:", application.bundleIdentifier)
- }
-
- self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
- self.sideloadingProgressView.observedProgress = nil
- self.sideloadingProgressView.setHidden(true, animated: true)
- }
- }
-
- DispatchQueue.main.async {
- self.sideloadingProgressView.progress = 0
- self.sideloadingProgressView.isHidden = false
- self.sideloadingProgressView.observedProgress = self.sideloadingProgress
- }
- }
- catch
- {
- try? FileManager.default.removeItem(at: temporaryDirectory)
-
- self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
- }
+ self.installApp(at: fileURL) { (result) in
+ print("Sideloaded app at \(fileURL) with result:", result)
}
}
}
diff --git a/AltStore/TabBarController.swift b/AltStore/TabBarController.swift
index 80d92f43..904c28a3 100644
--- a/AltStore/TabBarController.swift
+++ b/AltStore/TabBarController.swift
@@ -8,6 +8,17 @@
import UIKit
+extension TabBarController
+{
+ private enum Tab: Int, CaseIterable
+ {
+ case news
+ case browse
+ case myApps
+ case settings
+ }
+}
+
class TabBarController: UITabBarController
{
required init?(coder aDecoder: NSCoder)
@@ -15,6 +26,7 @@ class TabBarController: UITabBarController
super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
}
}
@@ -22,7 +34,11 @@ private extension TabBarController
{
@objc func openPatreonSettings(_ notification: Notification)
{
- guard let items = self.tabBar.items else { return }
- self.selectedIndex = items.count - 1
+ self.selectedIndex = Tab.settings.rawValue
+ }
+
+ @objc func importApp(_ notification: Notification)
+ {
+ self.selectedIndex = Tab.myApps.rawValue
}
}