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: diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 3460091f..87cd9574 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 = 18.6; 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..eed84aa1 100644 --- a/AltStore/LaunchViewController.swift +++ b/AltStore/LaunchViewController.swift @@ -11,83 +11,69 @@ import Roxas import minimuxer import WidgetKit +import AltSign 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) + detectAndImportAccountFile() guard let pf = fetchPairingFile() else { displayError("Device pairing file not found.") return @@ -95,216 +81,128 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg 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" - } - - // 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() - } - - 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 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) - } - - 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 { - displayError("Unable to read pairing file") - } - - // 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!) - } catch { - displayError("Unable to read pairing file") - } - - if (isSecuredURL) { - url.stopAccessingSecurityScopedResource() - } - 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) + 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!!!!!! REPORT TO GITHUB ISSUES!")") + try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName)) + displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR")") } 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 + func fetchPairingFile() -> String? { PairingFileManager.shared.fetchPairingFile(presentingVC: self) } + + func displayError(_ msg: String) { + print(msg) + 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 + defer { + if (isSecuredURL) { + url.stopAccessingSecurityScopedResource() + } } - 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") + + do { + let data = try Data(contentsOf: url) + guard let pairingString = String(data: data, encoding: .utf8) else { + displayError("Unable to read pairing file") + return } - 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) + try pairingString.write(to: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName), atomically: true, encoding: .utf8) + start_minimuxer_threads(pairingString) + } catch { + displayError("Unable to read pairing file") + } + + 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 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) } } - override func finishLaunching() - { - super.finishLaunching() - - guard !self.didFinishLaunching else { return } + func detectAndImportAccountFile() { + let accountFileURL = FileManager.default.documentsDirectory.appendingPathComponent("Account.sideconf") + #if !DEBUG + importAccountAtFile(accountFileURL, remove: true) + #else + importAccountAtFile(accountFileURL) + #endif + } +} + +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 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 + } + 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) + } + } + + @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 +210,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 +276,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..eeaa8657 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 @@ -251,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) } } @@ -290,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") @@ -298,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 @@ -328,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 @@ -1354,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/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/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? 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; \ diff --git a/SideStore/Tests/UITests/UITests.swift b/SideStore/Tests/UITests/UITests.swift index 0cf187fb..0486f4e9 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.exists || app.waitForExistence(timeout: 0.5) } let cellsQuery = collectionViewsQuery.cells @@ -302,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)) @@ -315,20 +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() + 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) + _ = appsSidestoreIoTextField.waitForExistence(timeout: 0.5) appsSidestoreIoTextField.tap() appsSidestoreIoTextField.tap() - _ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5) - collectionViewsQuery.staticTexts["Paste"].tap() + _ = 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.exists || app.waitForExistence(timeout: 0.5) } let cellsQuery = collectionViewsQuery.cells @@ -380,64 +503,27 @@ 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.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.5), " `+` 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.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() - - 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() - - 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) - } } 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" }, {