From d41a6b17d250c965a90c99fad095ebf42339da81 Mon Sep 17 00:00:00 2001 From: mahee96 <47920326+mahee96@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:22:35 +0530 Subject: [PATCH 1/8] iOS26: added support for iOS 26 deployment target + CI (fixed layout issues, added splash screen, fixed nav title insets). --- AltStore.xcodeproj/project.pbxproj | 8 +- AltStore/Base.lproj/Main.storyboard | 3 + AltStore/LaunchViewController.swift | 539 +++++++++--------- AltStore/Settings/Settings.storyboard | 3 + .../Settings/SettingsViewController.swift | 10 + AltStore/Sources/Sources.storyboard | 2 + AltStore/Sources/SourcesViewController.swift | 8 + Makefile | 18 +- 8 files changed, 304 insertions(+), 287 deletions(-) diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 3460091f..7d3c5fd5 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -3472,7 +3472,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; @@ -3500,7 +3500,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; @@ -3527,7 +3527,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; @@ -3554,7 +3554,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index d33cf363..a5e12d09 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -532,6 +532,7 @@ + @@ -561,6 +562,7 @@ + @@ -913,6 +915,7 @@ + diff --git a/AltStore/LaunchViewController.swift b/AltStore/LaunchViewController.swift index d10c291f..195dd2bf 100644 --- a/AltStore/LaunchViewController.swift +++ b/AltStore/LaunchViewController.swift @@ -10,301 +10,152 @@ import UIKit import Roxas import minimuxer import WidgetKit - import AltStoreCore import UniformTypeIdentifiers let pairingFileName = "ALTPairingFile.mobiledevicepairing" -final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate -{ +final class LaunchViewController: UIViewController, UIDocumentPickerDelegate { private var didFinishLaunching = false - - private var destinationViewController: TabBarController! - - override var launchConditions: [RSTLaunchCondition] { - let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in - DatabaseManager.shared.start(completionHandler: completionHandler) - } + private var retries = 0 + private var maxRetries = 3 + private var splashView: SplashView! + private var destinationViewController: TabBarController? + private var startTime: Date! - return [isDatabaseStarted] - } - - override var childForStatusBarStyle: UIViewController? { - return self.children.first - } - - override var childForStatusBarHidden: UIViewController? { - return self.children.first - } - - override func viewDidLoad() - { - defer { - // Create destinationViewController now so view controllers can register for receiving Notifications. - self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController - } + override func viewDidLoad() { super.viewDidLoad() + splashView = SplashView(frame: view.bounds, appName: "SideStore") + destinationViewController = storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController + view.addSubview(splashView) } - + override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(true) - if #available(iOS 17, *), !UserDefaults.standard.sidejitenable { - DispatchQueue.global().async { - self.isSideJITServerDetected() { result in - DispatchQueue.main.async { - switch result { - case .success(): - let dialogMessage = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert) - - // Create OK button with action handler - let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in - UserDefaults.standard.sidejitenable = true - }) - - let cancel = UIAlertAction(title: "Cancel", style: .cancel) - //Add OK button to a dialog message - dialogMessage.addAction(ok) - dialogMessage.addAction(cancel) - - // Present Alert to - self.present(dialogMessage, animated: true, completion: nil) - case .failure(_): - print("Cannot find sideJITServer") + super.viewDidAppear(animated) + guard !didFinishLaunching else { return } + Task { + startTime = Date() + await runLaunchSequence() + doPostLaunch() + } + } + + private func runLaunchSequence() async { + guard retries < maxRetries else { return } + retries += 1 + + await Task.detached { + if !DatabaseManager.shared.isStarted { + await withCheckedContinuation { continuation in + DatabaseManager.shared.start { error in + if let error { + Task { await self.handleLaunchError(error, retryCallback: self.runLaunchSequence) } + } else { + Task { await self.finishLaunching() } } + continuation.resume(returning: ()) } } + } else { + await self.finishLaunching() } - } - + }.value + } + + private func doPostLaunch() { + SideJITManager.shared.checkAndPromptIfNeeded(presentingVC: self) if #available(iOS 17, *), UserDefaults.standard.sidejitenable { - DispatchQueue.global().async { - self.askfornetwork() - } + DispatchQueue.global().async { SideJITManager.shared.askForNetwork() } print("SideJITServer Enabled") } - - - + #if !targetEnvironment(simulator) - - guard let pf = fetchPairingFile() else { + guard let pf = PairingFileManager.shared.fetchPairingFile(presentingVC: self) else { displayError("Device pairing file not found.") return } start_minimuxer_threads(pf) #endif } - - func askfornetwork() { - let address = UserDefaults.standard.textInputSideJITServerurl ?? "" - - var SJSURL = address - - if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty { - SJSURL = "http://sidejitserver._http._tcp.local:8080" + + func start_minimuxer_threads(_ pairing_file: String) { + target_minimuxer_address() + let documentsDirectory = FileManager.default.documentsDirectory.absoluteString + do { + let loggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled + try minimuxer.startWithLogger(pairing_file, documentsDirectory, loggingEnabled) + } catch { + try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName)) + displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR")") } - - // Create a network operation at launch to Refresh SideJITServer - let url = URL(string: "\(SJSURL)/re/")! - let task = URLSession.shared.dataTask(with: url) { (data, response, error) in - print(data) - } - task.resume() + start_auto_mounter(documentsDirectory) } - - func isSideJITServerDetected(completion: @escaping (Result) -> Void) { - let address = UserDefaults.standard.textInputSideJITServerurl ?? "" - - var SJSURL = address - - if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty { - SJSURL = "http://sidejitserver._http._tcp.local:8080" - } - - // Create a network operation at launch to Refresh SideJITServer - let url = URL(string: SJSURL)! - let task = URLSession.shared.dataTask(with: url) { (data, response, error) in - if let error = error { - print("No SideJITServer on Network") - completion(.failure(error)) - return - } - completion(.success(())) - } - task.resume() - return - } - - func fetchPairingFile() -> String? { - let filename = "ALTPairingFile.mobiledevicepairing" - let fm = FileManager.default - let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)") - if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty { - print("Loaded ALTPairingFile from \(documentsPath.path)") - return contents - } else if - let appResourcePath = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"), - fm.fileExists(atPath: appResourcePath.path), - let data = fm.contents(atPath: appResourcePath.path), - let contents = String(data: data, encoding: .utf8), - !contents.isEmpty, - !UserDefaults.standard.isPairingReset { - print("Loaded ALTPairingFile from \(appResourcePath.path)") - return contents - } else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{ - print("Loaded ALTPairingFile from Info.plist") - return plistString - } else { - // Show an alert explaining the pairing file - // Create new Alert - let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert) - - // Create OK button with action handler - let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in - // Try to load it from a file picker - var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil) - types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data)) - types.append(.xml) - let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types) - documentPickerController.shouldShowFileExtensions = true - documentPickerController.delegate = self - self.present(documentPickerController, animated: true, completion: nil) - UserDefaults.standard.isPairingReset = false - }) - - //Add "help" button to take user to wiki - let wikiOption = UIAlertAction(title: "Help", style: .default) { (action) in - let wikiURL: String = "https://docs.sidestore.io/docs/installation/pairing-file" - if let url = URL(string: wikiURL) { - UIApplication.shared.open(url) - } - sleep(2) - exit(0) - } - - //Add buttons to dialog message - dialogMessage.addAction(wikiOption) - dialogMessage.addAction(ok) - // Present Alert to - self.present(dialogMessage, animated: true, completion: nil) - - let dialogMessage2 = UIAlertController(title: "Analytics", message: "This app contains anonymous analytics for research and project development. By continuing to use this app, you are consenting to this data collection", preferredStyle: .alert) - - let ok2 = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in}) - - dialogMessage2.addAction(ok2) - self.present(dialogMessage2, animated: true, completion: nil) - - return nil - } - } + func fetchPairingFile() -> String? { PairingFileManager.shared.fetchPairingFile(presentingVC: self) } func displayError(_ msg: String) { print(msg) - // Create a new alert - let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert) - - // Present alert to user - self.present(dialogMessage, animated: true, completion: nil) + let alert = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert) + self.present(alert, animated: true) } - + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { let url = urls[0] let isSecuredURL = url.startAccessingSecurityScopedResource() == true do { - // Read to a string - let data1 = try Data(contentsOf: urls[0]) - let pairing_string = String(bytes: data1, encoding: .utf8) - if pairing_string == nil { + let data = try Data(contentsOf: url) + guard let pairingString = String(data: data, encoding: .utf8) else { displayError("Unable to read pairing file") + return } - - // Save to a file for next launch - let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)") - try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8) - - // Start minimuxer now that we have a file - start_minimuxer_threads(pairing_string!) + try pairingString.write(to: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName), atomically: true, encoding: .utf8) + start_minimuxer_threads(pairingString) } catch { displayError("Unable to read pairing file") } - - if (isSecuredURL) { - url.stopAccessingSecurityScopedResource() - } - controller.dismiss(animated: true, completion: nil) + + if isSecuredURL { url.stopAccessingSecurityScopedResource() } + controller.dismiss(animated: true) } - + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.") } - - func start_minimuxer_threads(_ pairing_file: String) { - target_minimuxer_address() - let documentsDirectory = FileManager.default.documentsDirectory.absoluteString - do { - // enable minimuxer console logging only if enabled in settings - let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled - try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled) - } catch { - try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")) - displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")") - } - start_auto_mounter(documentsDirectory) - // Create destinationViewController now so view controllers can register for receiving Notifications. - self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController - } } -extension LaunchViewController -{ - override func handleLaunchError(_ error: Error) - { - do - { - throw error - } - catch let error as NSError - { +extension LaunchViewController { + @MainActor + func handleLaunchError(_ error: Error, retryCallback: (() async -> Void)? = nil) { + do { throw error } catch let error as NSError { let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "") - - let errorDescription: String - - if #available(iOS 14.5, *) - { - let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription } - errorDescription = errorMessages.joined(separator: "\n\n") + let desc: String + if #available(iOS 14.5, *) { + desc = ([error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }).joined(separator: "\n\n") + } else { + desc = error.debugDescription } - else - { - errorDescription = error.debugDescription - } - - let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in - self.handleLaunchConditions() - })) - self.present(alertController, animated: true, completion: nil) + let alert = UIAlertController(title: title, message: desc, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default) { _ in + Task { await retryCallback?() } + }) + present(alert, animated: true) } } - - override func finishLaunching() - { - super.finishLaunching() - - guard !self.didFinishLaunching else { return } + + @MainActor + func finishLaunching() async { + guard !didFinishLaunching else { return } + didFinishLaunching = true AppManager.shared.update() AppManager.shared.updatePatronsIfNeeded() PatreonAPI.shared.refreshPatreonAccount() - AppManager.shared.updateAllSources { result in guard case .failure(let error) = result else { return } Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)") + let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError) print("Failed to update sources on launch. \(errorDesc)") @@ -312,63 +163,64 @@ extension LaunchViewController if String(describing: error).contains("The Internet connection appears to be offline"){ mode = .localizedDescription // dont make noise! } - let toastView = ToastView(error: error, mode: mode) toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside) - toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController) + toastView.show(in: self.destinationViewController!.selectedViewController ?? self.destinationViewController!) } - - self.updateKnownSources() - - // Ask widgets to be refreshed + updateKnownSources() WidgetCenter.shared.reloadAllTimelines() + didFinishLaunching = true - // Add view controller as child (rather than presenting modally) - // so tint adjustment + card presentations works correctly. - self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height) - self.destinationViewController.view.alpha = 0.0 - self.addChild(self.destinationViewController) - self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero) - self.destinationViewController.didMove(toParent: self) + let destinationVC = destinationViewController! - UIView.animate(withDuration: 0.2) { - self.destinationViewController.view.alpha = 1.0 - } + let elapsed = abs(startTime.timeIntervalSinceNow) + let remaining = elapsed >= 1 ? 0 : 1 - elapsed + try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000)) - self.didFinishLaunching = true - } -} + destinationVC.loadViewIfNeeded() + addChild(destinationVC) + destinationVC.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(destinationVC.view) + destinationVC.didMove(toParent: self) + + // Pin edges BEFORE animation + NSLayoutConstraint.activate([ + destinationVC.view.topAnchor.constraint(equalTo: view.topAnchor), + destinationVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + destinationVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + destinationVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) -private extension LaunchViewController -{ - func updateKnownSources() - { + // Set initial alpha for fade-in + destinationVC.view.alpha = 0 + + UIView.transition(with: view, duration: 0.3, options: .transitionCrossDissolve) { [self] in + self.splashView.alpha = 0 + destinationVC.view.alpha = 1 + } completion: { _ in + self.splashView.removeFromSuperview() + self.destinationViewController = destinationVC + } + } + + func updateKnownSources() { AppManager.shared.updateKnownSources { result in - switch result - { + switch result { case .failure(let error): print("[ALTLog] Failed to update known sources:", error) case .success((_, let blockedSources)): DatabaseManager.shared.persistentContainer.performBackgroundTask { context in let blockedSourceIDs = Set(blockedSources.lazy.map { $0.identifier }) let blockedSourceURLs = Set(blockedSources.lazy.compactMap { $0.sourceURL }) - - let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@", - #keyPath(Source.identifier), blockedSourceIDs, - #keyPath(Source.sourceURL), blockedSourceURLs) - - let sourceErrors = Source.all(satisfying: predicate, in: context).map { (source) in - let blockedSource = blockedSources.first { $0.identifier == source.identifier } - return SourceError.blocked(source, bundleIDs: blockedSource?.bundleIDs, existingSource: source) + let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@", #keyPath(Source.identifier), blockedSourceIDs, #keyPath(Source.sourceURL), blockedSourceURLs) + let sourceErrors = Source.all(satisfying: predicate, in: context).map { source in + let blocked = blockedSources.first { $0.identifier == source.identifier } + return SourceError.blocked(source, bundleIDs: blocked?.bundleIDs, existingSource: source) } - guard !sourceErrors.isEmpty else { return } - Task { - for error in sourceErrors - { + for error in sourceErrors { let title = String(format: NSLocalizedString("“%@” Blocked", comment: ""), error.$source.name) let message = [error.localizedDescription, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n\n") - await self.presentAlert(title: title, message: message) } } @@ -377,3 +229,142 @@ private extension LaunchViewController } } } + +// MARK: - SplashView +final class SplashView: UIView { + let iconView = UIImageView() + let titleLabel = UILabel() + + init(frame: CGRect, appName: String) { + super.init(frame: frame) + backgroundColor = .systemBackground + setupIcon() + setupTitle(appName: appName) + } + + required init?(coder: NSCoder) { fatalError() } + + private func setupIcon() { + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + container.layer.shadowColor = UIColor.black.cgColor + container.layer.shadowOpacity = 0.25 + container.layer.shadowOffset = CGSize(width: 0, height: 4) + container.layer.shadowRadius = 8 + addSubview(container) + + iconView.image = UIImage(named: "AppIcon") ?? UIImage(named: "AppIcon60x60") ?? UIImage(systemName: "app.fill") + iconView.contentMode = .scaleAspectFit + iconView.translatesAutoresizingMaskIntoConstraints = false + iconView.layer.cornerRadius = 24 + iconView.clipsToBounds = true + container.addSubview(iconView) + + NSLayoutConstraint.activate([ + container.centerXAnchor.constraint(equalTo: centerXAnchor), + container.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20), + container.widthAnchor.constraint(equalToConstant: 120), + container.heightAnchor.constraint(equalToConstant: 120), + iconView.topAnchor.constraint(equalTo: container.topAnchor), + iconView.bottomAnchor.constraint(equalTo: container.bottomAnchor), + iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + iconView.trailingAnchor.constraint(equalTo: container.trailingAnchor) + ]) + } + + private func setupTitle(appName: String) { + titleLabel.text = appName + titleLabel.font = .systemFont(ofSize: 24, weight: .bold) + titleLabel.textColor = .label + titleLabel.textAlignment = .center + titleLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 12), + titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor) + ]) + } +} + +// MARK: - PairingFileManager +final class PairingFileManager { + static let shared = PairingFileManager() + func fetchPairingFile(presentingVC: UIViewController) -> String? { + let fm = FileManager.default + let filename = pairingFileName + let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)") + if fm.fileExists(atPath: documentsPath.path), + let contents = try? String(contentsOf: documentsPath), !contents.isEmpty { + return contents + } + if let url = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"), + fm.fileExists(atPath: url.path), + let data = fm.contents(atPath: url.path), + let contents = String(data: data, encoding: .utf8), + !contents.isEmpty, !UserDefaults.standard.isPairingReset { return contents } + if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, + !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset { return plistString } + + presentPairingFileAlert(on: presentingVC) + return nil + } + + private func presentPairingFileAlert(on vc: UIViewController) { + let alert = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Help", style: .default) { _ in + if let url = URL(string: "https://docs.sidestore.io/docs/installation/pairing-file") { UIApplication.shared.open(url) } + sleep(2); exit(0) + }) + alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in + var types = UTType.types(tag: "plist", tagClass: .filenameExtension, conformingTo: nil) + types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: .filenameExtension, conformingTo: .data)) + types.append(.xml) + let picker = UIDocumentPickerViewController(forOpeningContentTypes: types) + picker.delegate = vc as? UIDocumentPickerDelegate + picker.shouldShowFileExtensions = true + vc.present(picker, animated: true) + UserDefaults.standard.isPairingReset = false + }) + vc.present(alert, animated: true) + } +} + +// MARK: - SideJITManager +final class SideJITManager { + static let shared = SideJITManager() + func checkAndPromptIfNeeded(presentingVC: UIViewController) { + guard #available(iOS 17, *), !UserDefaults.standard.sidejitenable else { return } + DispatchQueue.global().async { + self.isSideJITServerDetected { result in + DispatchQueue.main.async { + switch result { + case .success(): + let alert = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in UserDefaults.standard.sidejitenable = true }) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + presentingVC.present(alert, animated: true) + case .failure(_): print("Cannot find sideJITServer") + } + } + } + } + } + + func askForNetwork() { + let address = UserDefaults.standard.textInputSideJITServerurl ?? "" + let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address + URLSession.shared.dataTask(with: URL(string: "\(SJSURL)/re/")!) { data, resp, err in + print("data: \(String(describing: data)), response: \(String(describing: resp)), error: \(String(describing: err))") + }.resume() + } + + func isSideJITServerDetected(completion: @escaping (Result) -> Void) { + let address = UserDefaults.standard.textInputSideJITServerurl ?? "" + let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address + guard let url = URL(string: SJSURL) else { return } + URLSession.shared.dataTask(with: url) { _, _, error in + if let error = error { completion(.failure(error)); return } + completion(.success(())) + }.resume() + } +} diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index 9b1ea321..a60c7141 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -1450,6 +1450,8 @@ + + @@ -1923,6 +1925,7 @@ Settings by i cons from the Noun Project + diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift index f6c4cd8f..f4f16c05 100644 --- a/AltStore/Settings/SettingsViewController.swift +++ b/AltStore/Settings/SettingsViewController.swift @@ -202,6 +202,16 @@ final class SettingsViewController: UITableViewController { super.viewDidLoad() + // --- iOS 26 fix --- + if #available(iOS 26.0, *) { + let appearance = UINavigationBarAppearance() +// appearance.configureWithOpaqueBackground() // or .defaultBackground if you want blur +// appearance.backgroundColor = UIColor(named: "SettingsBackground") + appearance.titleTextAttributes = [.foregroundColor: UIColor.white] + appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white] + navigationController?.navigationBar.standardAppearance = appearance + navigationController?.navigationBar.scrollEdgeAppearance = appearance // required for iOS 26, maybe enforce it in storyboard? + } let nib = UINib(nibName: "SettingsHeaderFooterView", bundle: nil) self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView diff --git a/AltStore/Sources/Sources.storyboard b/AltStore/Sources/Sources.storyboard index d521df37..c8e5cf4e 100644 --- a/AltStore/Sources/Sources.storyboard +++ b/AltStore/Sources/Sources.storyboard @@ -20,6 +20,7 @@ + @@ -248,6 +249,7 @@ + diff --git a/AltStore/Sources/SourcesViewController.swift b/AltStore/Sources/SourcesViewController.swift index 5e9b80fa..8f74fd96 100644 --- a/AltStore/Sources/SourcesViewController.swift +++ b/AltStore/Sources/SourcesViewController.swift @@ -46,6 +46,14 @@ final class SourcesViewController: UICollectionViewController { super.viewDidLoad() + // Ensure large titles + navigationController?.navigationBar.prefersLargeTitles = true + navigationItem.largeTitleDisplayMode = .automatic + + // Set title + navigationItem.title = "Sources" + navigationController?.navigationBar.layoutMargins.left = 20 + let layout = self.makeLayout() self.collectionView.collectionViewLayout = layout diff --git a/Makefile b/Makefile index a5d17569..937e80d7 100755 --- a/Makefile +++ b/Makefile @@ -201,7 +201,7 @@ build-and-test: @echo "" @echo "Performing a build and running tests..." @xcodebuild test \ - -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \ + -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \ -resultBundlePath build/tests/test-results.xcresult \ -enableCodeCoverage YES \ $(COMMON_BUILD_SETTINGS) @@ -213,7 +213,7 @@ build-tests: @echo "Performing a build-for-testing..." @xcodebuild build-for-testing \ -enableCodeCoverage YES \ - -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \ + -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \ $(COMMON_BUILD_SETTINGS) run-tests: @@ -224,22 +224,22 @@ run-tests: @xcodebuild test-without-building \ -enableCodeCoverage YES \ -resultBundlePath build/tests/test-results.xcresult \ - -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \ + -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \ $(COMMON_BUILD_SETTINGS) boot-sim-async: - @if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \ - echo "Simulator 'iPhone 16 Pro' is already booted."; \ + @if xcrun simctl list devices "iPhone 17 Pro" | grep -q "Booted"; then \ + echo "Simulator 'iPhone 17 Pro' is already booted."; \ else \ - echo "Booting simulator 'iPhone 16 Pro' asynchronously..."; \ - xcrun simctl boot "iPhone 16 Pro" & \ + echo "Booting simulator 'iPhone 17 Pro' asynchronously..."; \ + xcrun simctl boot "iPhone 17 Pro" & \ echo "Simulator boot command dispatched."; \ fi sim-boot-check: @echo "Checking simulator boot status..." - @if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \ - echo "Simulator 'iPhone 16 Pro' is booted."; \ + @if xcrun simctl list devices "iPhone 17 Pro" | grep -q "Booted"; then \ + echo "Simulator 'iPhone 17 Pro' is booted."; \ else \ echo "Simulator bootup failed or is not booted yet."; \ exit 1; \ From b4df06f742b4574e89abc8d201b0eef0cf609474 Mon Sep 17 00:00:00 2001 From: mahee96 <47920326+mahee96@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:32:42 +0530 Subject: [PATCH 2/8] CI: updated to macOS 26 + xcode 26 for runners --- .github/.obsolete/reusable-build-workflow.yml | 16 ++++++++-------- .github/workflows/sidestore-build.yml | 4 ++-- .github/workflows/sidestore-tests-build.yml | 6 +++--- .github/workflows/sidestore-tests-run.yml | 6 +++--- .github/workflows/stable.yml | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/.obsolete/reusable-build-workflow.yml b/.github/.obsolete/reusable-build-workflow.yml index 2ca1382c..97d661cd 100644 --- a/.github/.obsolete/reusable-build-workflow.yml +++ b/.github/.obsolete/reusable-build-workflow.yml @@ -73,8 +73,8 @@ jobs: fail-fast: false matrix: include: - - os: 'macos-15' - version: '16.2' + - os: 'macos-26' + version: '26.0' runs-on: ${{ matrix.os }} outputs: @@ -426,8 +426,8 @@ jobs: fail-fast: false matrix: include: - - os: 'macos-15' - version: '16.2' + - os: 'macos-26' + version: '26.0' runs-on: ${{ matrix.os }} steps: @@ -443,7 +443,7 @@ jobs: - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1.6.0 with: - xcode-version: '16.2' + xcode-version: '26.0' # - name: (Tests-Build) Cache Build # uses: irgaly/xcode-cache@v1.8.1 @@ -610,8 +610,8 @@ jobs: fail-fast: false matrix: include: - - os: 'macos-15' - version: '16.2' + - os: 'macos-26' + version: '26.0' runs-on: ${{ matrix.os }} steps: @@ -628,7 +628,7 @@ jobs: - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1.6.0 with: - xcode-version: '16.2' + xcode-version: '26.0' # - name: (Tests-Run) Cache Build # uses: irgaly/xcode-cache@v1.8.1 diff --git a/.github/workflows/sidestore-build.yml b/.github/workflows/sidestore-build.yml index d030bc19..8fc74437 100644 --- a/.github/workflows/sidestore-build.yml +++ b/.github/workflows/sidestore-build.yml @@ -35,8 +35,8 @@ jobs: fail-fast: false matrix: include: - - os: 'macos-15' - version: '16.2' + - os: 'macos-26' + version: '26.0' runs-on: ${{ matrix.os }} outputs: version: ${{ steps.version.outputs.version }} diff --git a/.github/workflows/sidestore-tests-build.yml b/.github/workflows/sidestore-tests-build.yml index a0643366..4f8f6f6a 100644 --- a/.github/workflows/sidestore-tests-build.yml +++ b/.github/workflows/sidestore-tests-build.yml @@ -19,8 +19,8 @@ jobs: fail-fast: false matrix: include: - - os: 'macos-15' - version: '16.2' + - os: 'macos-26' + version: '26.0' runs-on: ${{ matrix.os }} steps: @@ -37,7 +37,7 @@ jobs: - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1.6.0 with: - xcode-version: '16.2' + xcode-version: '26.0' # - name: (Tests-Build) Cache Build # uses: irgaly/xcode-cache@v1.8.1 diff --git a/.github/workflows/sidestore-tests-run.yml b/.github/workflows/sidestore-tests-run.yml index bbb0640e..5e2318a0 100644 --- a/.github/workflows/sidestore-tests-run.yml +++ b/.github/workflows/sidestore-tests-run.yml @@ -19,8 +19,8 @@ jobs: fail-fast: false matrix: include: - - os: 'macos-15' - version: '16.2' + - os: 'macos-26' + version: '26.0' runs-on: ${{ matrix.os }} steps: @@ -38,7 +38,7 @@ jobs: - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1.6.0 with: - xcode-version: '16.2' + xcode-version: '26.0' # - name: (Tests-Run) Cache Build # uses: irgaly/xcode-cache@v1.8.1 diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml index b78bd466..68f1ea92 100644 --- a/.github/workflows/stable.yml +++ b/.github/workflows/stable.yml @@ -12,8 +12,8 @@ jobs: fail-fast: false matrix: include: - - os: 'macos-15' - version: '16.2' + - os: 'macos-26' + version: '26.0' runs-on: ${{ matrix.os }} steps: From 1d4666e79e0ad4a8d2b06551bc9383130acfb22b Mon Sep 17 00:00:00 2001 From: mahee96 <47920326+mahee96@users.noreply.github.com> Date: Wed, 15 Oct 2025 22:52:31 +0530 Subject: [PATCH 3/8] UITests: fixes for iOS 26 compatibility --- AltStore.xcodeproj/project.pbxproj | 2 +- SideStore/Tests/UITests/UITests.swift | 76 +++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 7d3c5fd5..87cd9574 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -3527,7 +3527,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; diff --git a/SideStore/Tests/UITests/UITests.swift b/SideStore/Tests/UITests/UITests.swift index 0cf187fb..97736ba6 100644 --- a/SideStore/Tests/UITests/UITests.swift +++ b/SideStore/Tests/UITests/UITests.swift @@ -19,7 +19,41 @@ final class UITests: XCTestCase { private static let APP_NAME = "SideStore" + func printAllMethods(of className: String) { + guard let cls: AnyClass = objc_getClass(className) as? AnyClass else { + print("Class \(className) not found") + return + } + + var methodCount: UInt32 = 0 + if let methodList = class_copyMethodList(cls, &methodCount) { + for i in 0.. 0 { appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed + _ = app.waitForExistence(timeout: 0.5) } let cellsQuery = collectionViewsQuery.cells @@ -318,17 +367,22 @@ private extension UITests { let app = XCUIApplication() app.tabBars["Tab Bar"].buttons["Sources"].tap() app.navigationBars["Sources"].buttons["Add"].tap() + _ = app.waitForExistence(timeout: 0.5) let collectionViewsQuery = app.collectionViews let appsSidestoreIoTextField = collectionViewsQuery.textFields["apps.sidestore.io"] _ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5) + _ = app.waitForExistence(timeout: 0.5) appsSidestoreIoTextField.tap() appsSidestoreIoTextField.tap() _ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5) collectionViewsQuery.staticTexts["Paste"].tap() + _ = app.waitForExistence(timeout: 0.5) + if app.keyboards.count > 0 { appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed + _ = app.waitForExistence(timeout: 0.5) } let cellsQuery = collectionViewsQuery.cells @@ -380,14 +434,24 @@ private extension UITests { .containing(.button, identifier: source.identifier) .children(matching: .button)[source.identifier] XCTAssert(sourceButton.exists || sourceButton.waitForExistence(timeout: 10), "Source preview for id: '\(source.alertTitle)' not found in the view") - + + _ = sourceButton.waitForExistence(timeout: 0.5) + // let addButton = sourceButton.children(matching: .button).firstMatch +// let addButton = sourceButton.descendants(matching: .button)["add"] +// XCTAssert(addButton.exists || addButton.waitForExistence(timeout: 0.3), " `+` button for id: '\(source.alertTitle)' not found in the preview container") +// addButton.tap() + let addButton = sourceButton.children(matching: .button)["add"] - XCTAssert(addButton.exists || addButton.waitForExistence(timeout: 0.3), " `+` button for id: '\(source.alertTitle)' not found in the preview container") - addButton.tap() + XCTAssert(addButton.waitForExistence(timeout: 1)) //TODO: fine tune down the value to make tests faster (but validate tests still works) +// addButton.tap() + + let coord = addButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) + coord.tap() if source.requiresSwipe { sourceButton.swipeUp(velocity: .slow) // Swipe up if needed. + _ = sourceButton.waitForExistence(timeout: 0.1) } } } @@ -396,7 +460,8 @@ private extension UITests { // Navigate to the Sources screen and open the Add Source view. app.tabBars["Tab Bar"].buttons["Sources"].tap() app.navigationBars["Sources"].buttons["Add"].tap() - + _ = app.waitForExistence(timeout: 0.5) + let cellsQuery = app.collectionViews.cells // Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen @@ -421,7 +486,8 @@ private extension UITests { // Navigate to the Sources screen and open the Add Source view. app.tabBars["Tab Bar"].buttons["Sources"].tap() app.navigationBars["Sources"].buttons["Add"].tap() - + _ = app.waitForExistence(timeout: 0.5) + let cellsQuery = app.collectionViews.cells // Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen From baf3594a8e4adf25405fee8393e8b9d5e9daef30 Mon Sep 17 00:00:00 2001 From: mahee96 <47920326+mahee96@users.noreply.github.com> Date: Wed, 15 Oct 2025 23:33:48 +0530 Subject: [PATCH 4/8] UITests: added more idling waits since we removed(mocked) quiescent idling wait from testing framework at runtime. --- SideStore/Tests/UITests/UITests.swift | 198 ++++++++++++++------------ 1 file changed, 109 insertions(+), 89 deletions(-) diff --git a/SideStore/Tests/UITests/UITests.swift b/SideStore/Tests/UITests/UITests.swift index 97736ba6..97d6fbcc 100644 --- a/SideStore/Tests/UITests/UITests.swift +++ b/SideStore/Tests/UITests/UITests.swift @@ -55,7 +55,7 @@ final class UITests: XCTestCase { // printAllMethods(of: "XCUIApplicationProcess") // Put setup code here. This method is called before the invocation of each test method in the class. -// Self.dismissSpotlight() + Self.dismissSpotlight() // Self.deleteMyApp() Self.deleteMyApp2() @@ -81,7 +81,9 @@ final class UITests: XCTestCase { // if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear") - systemAlert.scrollViews.otherElements.buttons["Allow"].tap() + let allowButton = systemAlert.scrollViews.otherElements.buttons["Allow"] + _ = allowButton.exists || allowButton.waitForExistence(timeout: 0.5) + allowButton.tap() // Do the actual validation try performBulkAddingRecommendedSources(for: app) @@ -97,8 +99,10 @@ final class UITests: XCTestCase { // if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear") - systemAlert.scrollViews.otherElements.buttons["Allow"].tap() - + let allowButton = systemAlert.scrollViews.otherElements.buttons["Allow"] + _ = allowButton.exists || allowButton.waitForExistence(timeout: 0.5) + allowButton.tap() + // Do the actual validation try performBulkAddingInputSources(for: app) } @@ -112,8 +116,10 @@ final class UITests: XCTestCase { // if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear") - systemAlert.scrollViews.otherElements.buttons["Allow"].tap() - + let allowButton = systemAlert.scrollViews.otherElements.buttons["Allow"] + _ = allowButton.exists || allowButton.waitForExistence(timeout: 0.5) + allowButton.tap() + // Do the actual validation try performRepeatabilityForStagingInputSources(for: app) } @@ -127,8 +133,10 @@ final class UITests: XCTestCase { // if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear") - systemAlert.scrollViews.otherElements.buttons["Allow"].tap() - + let allowButton = systemAlert.scrollViews.otherElements.buttons["Allow"] + _ = allowButton.exists || allowButton.waitForExistence(timeout: 0.5) + allowButton.tap() + // Do the actual validation try performRepeatabilityForStagingRecommendedSources(for: app) } @@ -213,19 +221,19 @@ private extension UITests { let button = springboard_app.buttons["Remove App"] _ = button.exists || button.waitForExistence(timeout: 5) button.tap() - _ = springboard_app.waitForExistence(timeout: 0.3) + _ = springboard_app.waitForExistence(timeout: 0.5) } do { let button = springboard_app.buttons["Delete App"] - _ = button.waitForExistence(timeout: 0.3) + _ = button.waitForExistence(timeout: 0.5) button.tap() - _ = springboard_app.waitForExistence(timeout: 0.3) + _ = springboard_app.waitForExistence(timeout: 0.5) } do { let button = springboard_app.buttons["Delete"] - _ = button.waitForExistence(timeout: 0.3) + _ = button.waitForExistence(timeout: 0.5) button.tap() - _ = springboard_app.waitForExistence(timeout: 0.3) + _ = springboard_app.waitForExistence(timeout: 0.5) } // // Press home once to make the icons stop wiggling @@ -275,20 +283,50 @@ private extension UITests { try tapAddForThesePickedSources(app: app, sourceMappings: sourceMappings, cellsQuery: cellsQuery) // Commit the changes by tapping "Done". - app.navigationBars["Add Source"].buttons["Done"].tap() + let doneButton = app.navigationBars["Add Source"].buttons["Done"] + _ = doneButton.waitForExistence(timeout: 0.5) + doneButton.tap() // Accept each source addition via alert. for source in sourceMappings { let alertIdentifier = "Would you like to add the source “\(source.alertTitle)”?" let addSourceButton = app.alerts[alertIdentifier] .scrollViews.otherElements.buttons["Add Source"] - _ = addSourceButton.exists || addSourceButton.waitForExistence(timeout: 0.3) + _ = addSourceButton.waitForExistence(timeout: 0.5) addSourceButton.tap() } } - + private func performBulkAddingRecommendedSources(for app: XCUIApplication) throws { + // Navigate to the Sources screen and open the Add Source view. + let srcTab = app.tabBars["Tab Bar"].buttons["Sources"] + _ = srcTab.waitForExistence(timeout: 0.5) + srcTab.tap() + let srcAdd = app.navigationBars["Sources"].buttons["Add"] + _ = srcAdd.waitForExistence(timeout: 0.5) + srcAdd.tap() + + let cellsQuery = app.collectionViews.cells + + // Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen + let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [ + ("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false), + ("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false), + ("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false), + ("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", true), + ("UTM Repository\nVirtual machines for iOS", "UTM Repository", false), + ("Flyinghead\nflyinghead.github.io/flycast-builds/altstore.json", "Flyinghead", false), +// ("PojavLauncher Repository\nalt.crystall1ne.dev", "PojavLauncher Repository", false), // not a stable source, sometimes becomes unreachable, so disabled + ("PokeMMO\npokemmo.eu/altstore/", "PokeMMO", true), + ("Odyssey\ntheodyssey.dev/altstore/odysseysource.json", "Odyssey", false), + ("Yattee\nrepos.yattee.stream/alt/apps.json", "Yattee", false), + ("ThatStella7922 Source\nThe home for all apps ThatStella7922", "ThatStella7922 Source", false) + ] + + try performBulkAdd(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery) + } + private func performBulkAddingInputSources(for app: XCUIApplication) throws { // set content into clipboard (for bulk add (paste)) @@ -305,20 +343,23 @@ private extension UITests { https://bit.ly/Quantumsource """.trimmedIndentation - let app = XCUIApplication() - app.tabBars["Tab Bar"].buttons["Sources"].tap() - app.navigationBars["Sources"].buttons["Add"].tap() - _ = app.waitForExistence(timeout: 0.5) + let srcTab = app.tabBars["Tab Bar"].buttons["Sources"] + _ = srcTab.waitForExistence(timeout: 0.5) + srcTab.tap() + let srcAdd = app.navigationBars["Sources"].buttons["Add"] + _ = srcAdd.waitForExistence(timeout: 0.5) + srcAdd.tap() let collectionViewsQuery = app.collectionViews let appsSidestoreIoTextField = collectionViewsQuery.textFields["apps.sidestore.io"] - _ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5) - _ = app.waitForExistence(timeout: 0.5) + _ = appsSidestoreIoTextField.waitForExistence(timeout: 0.5) appsSidestoreIoTextField.tap() appsSidestoreIoTextField.tap() - collectionViewsQuery.staticTexts["Paste"].tap() - _ = app.waitForExistence(timeout: 0.5) - + _ = appsSidestoreIoTextField.waitForExistence(timeout: 0.5) + let pasteButton = collectionViewsQuery.staticTexts["Paste"] + _ = pasteButton.waitForExistence(timeout: 0.5) + pasteButton.tap() + // if app.keyboards.buttons["Return"].exists { // app.keyboards.buttons["Return"].tap() // } else if app.keyboards.buttons["Done"].exists { @@ -330,7 +371,7 @@ private extension UITests { if app.keyboards.count > 0 { appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed - _ = app.waitForExistence(timeout: 0.5) + _ = app.exists || app.waitForExistence(timeout: 0.5) } let cellsQuery = collectionViewsQuery.cells @@ -351,7 +392,32 @@ private extension UITests { try performBulkAdd(app: app, sourceMappings: textInputSources, cellsQuery: cellsQuery) } - + private func performRepeatabilityForStagingRecommendedSources(for app: XCUIApplication) throws { + // Navigate to the Sources screen and open the Add Source view. + let srcTab = app.tabBars["Tab Bar"].buttons["Sources"] + srcTab.tap() + _ = srcTab.waitForExistence(timeout: 0.5) + let srcAdd = app.navigationBars["Sources"].buttons["Add"] + srcAdd.tap() + _ = srcAdd.waitForExistence(timeout: 0.5) + + let cellsQuery = app.collectionViews.cells + + // Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen + let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [ + ("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false), + ("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false), + ("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false), + ("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", false), + ("UTM Repository\nVirtual machines for iOS", "UTM Repository", false), + ] + + let repeatCount = 3 // number of times to run the entire sequence + let timeSeed = UInt64(Date().timeIntervalSince1970) // time is unique (upto microseconds) - uncomment this to use non-deterministic seed based RNG (random number generator) + + try repeatabilityTest(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery, repeatCount: repeatCount, seed: timeSeed) + } + private func performRepeatabilityForStagingInputSources(for app: XCUIApplication) throws { // set content into clipboard (for bulk add (paste)) @@ -364,25 +430,28 @@ private extension UITests { https://bit.ly/40Isul6 """.trimmedIndentation - let app = XCUIApplication() - app.tabBars["Tab Bar"].buttons["Sources"].tap() - app.navigationBars["Sources"].buttons["Add"].tap() - _ = app.waitForExistence(timeout: 0.5) + let srcTab = app.tabBars["Tab Bar"].buttons["Sources"] + _ = srcTab.waitForExistence(timeout: 0.5) + srcTab.tap() + let srcAdd = app.navigationBars["Sources"].buttons["Add"] + _ = srcAdd.waitForExistence(timeout: 0.5) + srcAdd.tap() + let collectionViewsQuery = app.collectionViews let appsSidestoreIoTextField = collectionViewsQuery.textFields["apps.sidestore.io"] - _ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5) - _ = app.waitForExistence(timeout: 0.5) + _ = appsSidestoreIoTextField.waitForExistence(timeout: 0.5) appsSidestoreIoTextField.tap() appsSidestoreIoTextField.tap() - _ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5) - collectionViewsQuery.staticTexts["Paste"].tap() - _ = app.waitForExistence(timeout: 0.5) + _ = appsSidestoreIoTextField.waitForExistence(timeout: 0.5) + let pasteButton = collectionViewsQuery.staticTexts["Paste"] + _ = pasteButton.waitForExistence(timeout: 0.5) + pasteButton.tap() if app.keyboards.count > 0 { appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed - _ = app.waitForExistence(timeout: 0.5) + _ = app.exists || app.waitForExistence(timeout: 0.5) } let cellsQuery = collectionViewsQuery.cells @@ -435,11 +504,11 @@ private extension UITests { .children(matching: .button)[source.identifier] XCTAssert(sourceButton.exists || sourceButton.waitForExistence(timeout: 10), "Source preview for id: '\(source.alertTitle)' not found in the view") - _ = sourceButton.waitForExistence(timeout: 0.5) + _ = sourceButton.exists || sourceButton.waitForExistence(timeout: 0.5) // let addButton = sourceButton.children(matching: .button).firstMatch // let addButton = sourceButton.descendants(matching: .button)["add"] -// XCTAssert(addButton.exists || addButton.waitForExistence(timeout: 0.3), " `+` button for id: '\(source.alertTitle)' not found in the preview container") +// XCTAssert(addButton.exists || addButton.waitForExistence(timeout: 0.5), " `+` button for id: '\(source.alertTitle)' not found in the preview container") // addButton.tap() let addButton = sourceButton.children(matching: .button)["add"] @@ -451,59 +520,10 @@ private extension UITests { if source.requiresSwipe { sourceButton.swipeUp(velocity: .slow) // Swipe up if needed. - _ = sourceButton.waitForExistence(timeout: 0.1) + _ = sourceButton.waitForExistence(timeout: 0.5) } } } - - private func performBulkAddingRecommendedSources(for app: XCUIApplication) throws { - // Navigate to the Sources screen and open the Add Source view. - app.tabBars["Tab Bar"].buttons["Sources"].tap() - app.navigationBars["Sources"].buttons["Add"].tap() - _ = app.waitForExistence(timeout: 0.5) - - let cellsQuery = app.collectionViews.cells - - // Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen - let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [ - ("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false), - ("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false), - ("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false), - ("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", true), - ("UTM Repository\nVirtual machines for iOS", "UTM Repository", false), - ("Flyinghead\nflyinghead.github.io/flycast-builds/altstore.json", "Flyinghead", false), -// ("PojavLauncher Repository\nalt.crystall1ne.dev", "PojavLauncher Repository", false), // not a stable source, sometimes becomes unreachable, so disabled - ("PokeMMO\npokemmo.eu/altstore/", "PokeMMO", true), - ("Odyssey\ntheodyssey.dev/altstore/odysseysource.json", "Odyssey", false), - ("Yattee\nrepos.yattee.stream/alt/apps.json", "Yattee", false), - ("ThatStella7922 Source\nThe home for all apps ThatStella7922", "ThatStella7922 Source", false) - ] - - try performBulkAdd(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery) - } - - private func performRepeatabilityForStagingRecommendedSources(for app: XCUIApplication) throws { - // Navigate to the Sources screen and open the Add Source view. - app.tabBars["Tab Bar"].buttons["Sources"].tap() - app.navigationBars["Sources"].buttons["Add"].tap() - _ = app.waitForExistence(timeout: 0.5) - - let cellsQuery = app.collectionViews.cells - - // Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen - let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [ - ("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false), - ("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false), - ("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false), - ("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", false), - ("UTM Repository\nVirtual machines for iOS", "UTM Repository", false), - ] - - let repeatCount = 3 // number of times to run the entire sequence - let timeSeed = UInt64(Date().timeIntervalSince1970) // time is unique (upto microseconds) - uncomment this to use non-deterministic seed based RNG (random number generator) - - try repeatabilityTest(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery, repeatCount: repeatCount, seed: timeSeed) - } } From dc53f19947c4f4fe02f99dd8e4dddb310d07c79b Mon Sep 17 00:00:00 2001 From: mahee96 <47920326+mahee96@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:16:11 +0530 Subject: [PATCH 5/8] UITests: disable spotlight dismiss which is causing failures in test --- SideStore/Tests/UITests/UITests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SideStore/Tests/UITests/UITests.swift b/SideStore/Tests/UITests/UITests.swift index 97d6fbcc..0486f4e9 100644 --- a/SideStore/Tests/UITests/UITests.swift +++ b/SideStore/Tests/UITests/UITests.swift @@ -55,7 +55,7 @@ final class UITests: XCTestCase { // printAllMethods(of: "XCUIApplicationProcess") // Put setup code here. This method is called before the invocation of each test method in the class. - Self.dismissSpotlight() +// Self.dismissSpotlight() // Self.deleteMyApp() Self.deleteMyApp2() From a0ae0cb2b18ca78ff15b1f385665de2f32452141 Mon Sep 17 00:00:00 2001 From: nythepegasus Date: Wed, 22 Oct 2025 17:04:05 -0400 Subject: [PATCH 6/8] feat: Updated deprecated Pojav identifier to new Amethyst repo Signed-off-by: nythepegasus --- trustedapps.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trustedapps.json b/trustedapps.json index 69c08e8b..4da942b0 100644 --- a/trustedapps.json +++ b/trustedapps.json @@ -29,7 +29,7 @@ "sourceURL": "https://flyinghead.github.io/flycast-builds/altstore.json" }, { - "identifier": "dev.crystall1ne.repos.PojavLauncher", + "identifier": "dev.crystall1ne.alt", "sourceURL": "https://alt.crystall1ne.dev" }, { @@ -82,7 +82,7 @@ "sourceURL": "https://flyinghead.github.io/flycast-builds/altstore.json" }, { - "identifier": "dev.crystall1ne.repos.PojavLauncher", + "identifier": "dev.crystall1ne.alt", "sourceURL": "https://alt.crystall1ne.dev" }, { From 15ffe766d3d71839c67375779f07485e437635b5 Mon Sep 17 00:00:00 2001 From: ny Date: Fri, 7 Nov 2025 16:52:55 -0500 Subject: [PATCH 7/8] fix: Account exporting/importing --- AltStore/LaunchViewController.swift | 71 ++++++++++++++++- .../Settings/SettingsViewController.swift | 79 +++++++++++-------- AltStore/Types/ImportedAccount.swift | 1 - AltStoreCore/Components/Keychain.swift | 3 - 4 files changed, 112 insertions(+), 42 deletions(-) diff --git a/AltStore/LaunchViewController.swift b/AltStore/LaunchViewController.swift index 195dd2bf..d3fde636 100644 --- a/AltStore/LaunchViewController.swift +++ b/AltStore/LaunchViewController.swift @@ -10,6 +10,8 @@ import UIKit import Roxas import minimuxer import WidgetKit + +import AltSign import AltStoreCore import UniformTypeIdentifiers @@ -70,7 +72,9 @@ final class LaunchViewController: UIViewController, UIDocumentPickerDelegate { } #if !targetEnvironment(simulator) - guard let pf = PairingFileManager.shared.fetchPairingFile(presentingVC: self) else { + + detectAndImportAccountFile() + guard let pf = fetchPairingFile() else { displayError("Device pairing file not found.") return } @@ -102,6 +106,11 @@ final class LaunchViewController: UIViewController, UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { let url = urls[0] let isSecuredURL = url.startAccessingSecurityScopedResource() == true + defer { + if (isSecuredURL) { + url.stopAccessingSecurityScopedResource() + } + } do { let data = try Data(contentsOf: url) @@ -114,14 +123,68 @@ final class LaunchViewController: UIViewController, UIDocumentPickerDelegate { } catch { displayError("Unable to read pairing file") } - - if isSecuredURL { url.stopAccessingSecurityScopedResource() } - controller.dismiss(animated: true) + + controller.dismiss(animated: true, completion: nil) } func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.") } + + func start_minimuxer_threads(_ pairing_file: String) { + target_minimuxer_address() + let documentsDirectory = FileManager.default.documentsDirectory.absoluteString + do { + // enable minimuxer console logging only if enabled in settings + let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled + try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled) + } catch { + try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")) + displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")") + } + start_auto_mounter(documentsDirectory) + // Create destinationViewController now so view controllers can register for receiving Notifications. + self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController + } + + func importAccountAtFile(_ file: URL, remove: Bool = false) { + _ = file.startAccessingSecurityScopedResource() + defer { file.stopAccessingSecurityScopedResource() } + guard let accountD = try? Data(contentsOf: file) else { + let toastView = ToastView(text: NSLocalizedString("Could not read data from file!", comment: ""), detailText: "\(file)") + return toastView.show(in: self) + } + guard let account = try? Foundation.JSONDecoder().decode(ImportedAccount.self, from: accountD) else { + let toastView = ToastView(text: NSLocalizedString("Could not parse data from file!", comment: ""), detailText: "\(file)") + return toastView.show(in: self) + } + print("We want to import this account probably: \(account)") + if remove { + try? FileManager.default.removeItem(at: file) + } + Keychain.shared.appleIDEmailAddress = account.email + Keychain.shared.appleIDPassword = account.password + Keychain.shared.adiPb = account.adiPB + Keychain.shared.identifier = account.local_user + if let altCert = ALTCertificate(p12Data: account.cert, password: account.certpass) { + Keychain.shared.signingCertificate = altCert.encryptedP12Data(withPassword: "")! + Keychain.shared.signingCertificatePassword = account.certpass + let toastView = ToastView(text: NSLocalizedString("Successfully imported '\(account.email)'!", comment: ""), detailText: "SideStore should be fully operational!") + return toastView.show(in: self) + } else { + let toastView = ToastView(text: NSLocalizedString("Failed to import account certificate!", comment: ""), detailText: "Failed to create ALTCertificate. Check if the password is correct. Still imported account/adi.pb details!") + return toastView.show(in: self) + } + } + + func detectAndImportAccountFile() { + let accountFileURL = FileManager.default.documentsDirectory.appendingPathComponent("Account.sideconf") + #if !DEBUG + importAccountAtFile(accountFileURL, remove: true) + #else + importAccountAtFile(accountFileURL) + #endif + } } extension LaunchViewController { diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift index f4f16c05..eeaa8657 100644 --- a/AltStore/Settings/SettingsViewController.swift +++ b/AltStore/Settings/SettingsViewController.swift @@ -261,28 +261,34 @@ final class SettingsViewController: UITableViewController } func importAccountAtFile(_ file: URL, remove: Bool = false) { - if let accountData = try? Data(contentsOf: file), - let account = try? Foundation.JSONDecoder().decode(ImportedAccount.self, from: accountData) { - print("We want to import this account probably: \(account)") - if remove { - try? FileManager.default.removeItem(at: file) - } - Keychain.shared.appleIDEmailAddress = account.email - Keychain.shared.appleIDPassword = account.password - Keychain.shared.adiPb = account.adiPB - Keychain.shared.adiSerial = account.serial - Keychain.shared.identifier = account.local_user - signIn() - update() - if let altCert = ALTCertificate(p12Data: account.cert, password: account.certpass) { - Keychain.shared.signingCertificate = altCert.encryptedP12Data(withPassword: "")! - Keychain.shared.signingCertificatePassword = account.certpass - let toastView = ToastView(text: NSLocalizedString("Successfully imported '\(account.email)'!", comment: ""), detailText: "SideStore should be fully operational!") - return toastView.show(in: self) - } else { - let toastView = ToastView(text: NSLocalizedString("Failed to import account certificate!", comment: ""), detailText: "Failed to create ALTCertificate. Check if the password is correct. Still imported account/adi.pb details!") - return toastView.show(in: self) - } + _ = file.startAccessingSecurityScopedResource() + defer { file.stopAccessingSecurityScopedResource() } + guard let accountD = try? Data(contentsOf: file) else { + let toastView = ToastView(text: NSLocalizedString("Could not read data from file!", comment: ""), detailText: "\(file)") + return toastView.show(in: self) + } + guard let account = try? Foundation.JSONDecoder().decode(ImportedAccount.self, from: accountD) else { + let toastView = ToastView(text: NSLocalizedString("Could not parse data from file!", comment: ""), detailText: "\(file)") + return toastView.show(in: self) + } + print("We want to import this account probably: \(account)") + if remove { + try? FileManager.default.removeItem(at: file) + } + Keychain.shared.appleIDEmailAddress = account.email + Keychain.shared.appleIDPassword = account.password + Keychain.shared.adiPb = account.adiPB + Keychain.shared.identifier = account.local_user + signIn() + update() + if let altCert = ALTCertificate(p12Data: account.cert, password: account.certpass) { + Keychain.shared.signingCertificate = altCert.encryptedP12Data(withPassword: "")! + Keychain.shared.signingCertificatePassword = account.certpass + let toastView = ToastView(text: NSLocalizedString("Successfully imported '\(account.email)'!", comment: ""), detailText: "SideStore should be fully operational!") + return toastView.show(in: self) + } else { + let toastView = ToastView(text: NSLocalizedString("Failed to import account certificate!", comment: ""), detailText: "Failed to create ALTCertificate. Check if the password is correct. Still imported account/adi.pb details!") + return toastView.show(in: self) } } @@ -300,7 +306,6 @@ final class SettingsViewController: UITableViewController let password = Keychain.shared.appleIDPassword, let cert = Keychain.shared.signingCertificate, let identifier = Keychain.shared.identifier, - let adiSerial = Keychain.shared.adiSerial, let adiPB = Keychain.shared.adiPb else { #if DEBUG print(Keychain.shared.appleIDEmailAddress ?? "Empty email") @@ -308,17 +313,16 @@ final class SettingsViewController: UITableViewController print(Keychain.shared.signingCertificate ?? "Empty cert") print(Keychain.shared.identifier ?? "Empty identifier") print(Keychain.shared.adiPb ?? "Empty adiPb") - print(Keychain.shared.adiSerial ?? "Empty adiSerial") #endif return nil } - return ImportedAccount(email: email, password: password, cert: cert, certpass: certpass, local_user: identifier, serial: adiSerial, adiPB: adiPB) + return ImportedAccount(email: email, password: password, cert: cert, certpass: certpass, local_user: identifier, adiPB: adiPB) } func showExportAccount() { Task { - let password = await withUnsafeContinuation { (c: UnsafeContinuation) in + guard let password = await withUnsafeContinuation({ (c: UnsafeContinuation) in let alertController = UIAlertController(title: NSLocalizedString("Please enter the password for the certificate.", comment: ""), message: nil, preferredStyle: .alert) alertController.addTextField { (textField) in @@ -338,19 +342,16 @@ final class SettingsViewController: UITableViewController }) self.present(alertController, animated: true) - } - - guard let password else { + }) else { return } guard let account = exportAccount(password) else { let toastView = ToastView(text: NSLocalizedString("Failed to export account!", comment: ""), detailText: "Account not found.") - toastView.show(in: self) - return + return toastView.show(in: self) } - guard let accountData = try? Foundation.JSONEncoder().encode(account).base64EncodedData() else { + guard let accountData = try? Foundation.JSONEncoder().encode(account) else { let toastView = ToastView(text: NSLocalizedString("Failed to export account data!", comment: ""), detailText: "Account malformed.") toastView.show(in: self) return @@ -1364,15 +1365,25 @@ extension SettingsViewController guard let confUrl else { return } - _ = confUrl.startAccessingSecurityScopedResource() - defer { confUrl.stopAccessingSecurityScopedResource() } importAccountAtFile(confUrl) } case .importCert: + let importVc = UIDocumentPickerViewController(forOpeningContentTypes: [UTType(filenameExtension: "p12")!], asCopy: false) + ImportExport.documentPickerHandler = DocumentPickerHandler { url in + guard let url else { + return + } + importVc.delegate = ImportExport.documentPickerHandler + self.present(importVc, animated: true) + _ = url.startAccessingSecurityScopedResource() + defer { url.stopAccessingSecurityScopedResource() } + } Task { let certUrl = await withUnsafeContinuation { c in let importVc = UIDocumentPickerViewController(forOpeningContentTypes: [UTType(filenameExtension: "p12")!], asCopy: false) ImportExport.documentPickerHandler = DocumentPickerHandler { url in + _ = url?.startAccessingSecurityScopedResource() + defer { url?.stopAccessingSecurityScopedResource() } c.resume(returning: url) } importVc.delegate = ImportExport.documentPickerHandler diff --git a/AltStore/Types/ImportedAccount.swift b/AltStore/Types/ImportedAccount.swift index c0e16060..ff47a48f 100644 --- a/AltStore/Types/ImportedAccount.swift +++ b/AltStore/Types/ImportedAccount.swift @@ -14,6 +14,5 @@ struct ImportedAccount: Codable { let cert: Data let certpass: String let local_user: String - let serial: String let adiPB: String } diff --git a/AltStoreCore/Components/Keychain.swift b/AltStoreCore/Components/Keychain.swift index b6543582..efc2808d 100644 --- a/AltStoreCore/Components/Keychain.swift +++ b/AltStoreCore/Components/Keychain.swift @@ -80,9 +80,6 @@ public class Keychain @KeychainItem(key: "identifier") public var identifier: String? - @KeychainItem(key: "adiSerial") - public var adiSerial: String? - @KeychainItem(key: "adiPb") public var adiPb: String? From cfd9247fddb610e6a53806f5c2a2b880ffd16614 Mon Sep 17 00:00:00 2001 From: ny Date: Fri, 7 Nov 2025 17:17:08 -0500 Subject: [PATCH 8/8] fix: copied too much (*sigh* UIKit) --- AltStore/LaunchViewController.swift | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/AltStore/LaunchViewController.swift b/AltStore/LaunchViewController.swift index d3fde636..eed84aa1 100644 --- a/AltStore/LaunchViewController.swift +++ b/AltStore/LaunchViewController.swift @@ -131,22 +131,6 @@ final class LaunchViewController: UIViewController, UIDocumentPickerDelegate { displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.") } - func start_minimuxer_threads(_ pairing_file: String) { - target_minimuxer_address() - let documentsDirectory = FileManager.default.documentsDirectory.absoluteString - do { - // enable minimuxer console logging only if enabled in settings - let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled - try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled) - } catch { - try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")) - displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")") - } - start_auto_mounter(documentsDirectory) - // Create destinationViewController now so view controllers can register for receiving Notifications. - self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController - } - func importAccountAtFile(_ file: URL, remove: Bool = false) { _ = file.startAccessingSecurityScopedResource() defer { file.stopAccessingSecurityScopedResource() }