// // StikJITApp.swift // StikJIT // // Created by Stephen on 3/26/25. // import SwiftUI import Network import UniformTypeIdentifiers import NetworkExtension // Register default settings before the app starts private func registerAdvancedOptionsDefault() { let os = ProcessInfo.processInfo.operatingSystemVersion // Enable advanced options by default on iOS 19/26 and above let enabled = os.majorVersion >= 19 let defaults = UserDefaults.standard defaults.register(defaults: ["enableAdvancedOptions": enabled]) defaults.register(defaults: [UserDefaults.Keys.txmOverride: false]) if defaults.object(forKey: UserDefaults.Keys.enableContinuedProcessing) == nil { let legacyPiP = defaults.object(forKey: "enablePiP") as? Bool if let legacyPiP { defaults.set(legacyPiP, forKey: UserDefaults.Keys.enableContinuedProcessing) } else { defaults.register(defaults: [UserDefaults.Keys.enableContinuedProcessing: enabled]) } } } // MARK: - Welcome Sheet struct WelcomeSheetView: View { var onDismiss: (() -> Void)? @Environment(\.colorScheme) private var colorScheme @AppStorage("customAccentColor") private var customAccentColorHex: String = "" @Environment(\.themeExpansionManager) private var themeExpansion private var accent: Color { themeExpansion?.resolvedAccentColor(from: customAccentColorHex) ?? .blue } var body: some View { ZStack { // Background now comes from global BackgroundContainer Color.clear.ignoresSafeArea() ScrollView { VStack(spacing: 20) { // Card container with glassy material and stroke VStack(alignment: .leading, spacing: 16) { // Title Text("Welcome!") .font(.system(.largeTitle, design: .rounded).weight(.bold)) .foregroundColor(.primary) .padding(.top, 8) // Intro Text("Thanks for installing the app. This brief introduction will help you get started.") .font(.body) .foregroundColor(.secondary) .multilineTextAlignment(.leading) // App description VStack(alignment: .leading, spacing: 6) { Label("On‑device debugger", systemImage: "bolt.shield.fill") .foregroundColor(accent) .font(.headline) Text("StikDebug is an on‑device debugger designed specifically for self‑developed apps. It helps streamline testing and troubleshooting without sending any data to external servers.") .font(.callout) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } // VPN explanation VStack(alignment: .leading, spacing: 6) { Label("Why VPN permission?", systemImage: "lock.shield.fill") .foregroundColor(accent) .font(.headline) Text("The next step will prompt you to allow VPN permissions. This is necessary for the app to function properly. The VPN configuration allows your device to securely connect to itself — nothing more. No data is collected or sent externally; everything stays on your device.") .font(.callout) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } // Continue button Button(action: { onDismiss?() }) { Text("Continue") .font(.system(size: 16, weight: .semibold, design: .rounded)) .foregroundColor(accent.contrastText()) .frame(height: 44) .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(accent) ) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.primary.opacity(0.15), lineWidth: 1) ) } .padding(.top, 8) .accessibilityIdentifier("welcome_continue_button") } .padding(20) .background( RoundedRectangle(cornerRadius: 20, style: .continuous) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 20, style: .continuous) .strokeBorder(Color.primary.opacity(0.15), lineWidth: 1) ) ) .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) .shadow(color: .black.opacity(colorScheme == .dark ? 0.15 : 0.08), radius: 12, x: 0, y: 4) // Footer version info for consistency HStack { Spacer() Text("iOS \(UIDevice.current.systemVersion)") .font(.footnote) .foregroundColor(.secondary) Spacer() } .padding(.top, 6) } .padding(.horizontal, 20) .padding(.vertical, 30) } } // Inherit preferredColorScheme from BackgroundContainer (no local override) } } // MARK: - VPN Logger class VPNLogger: ObservableObject { @Published var logs: [String] = [] static var shared = VPNLogger() private init() {} func log(_ message: Any, file: String = #file, function: String = #function, line: Int = #line) { #if DEBUG let fileName = (file as NSString).lastPathComponent print("[\(fileName):\(line)] \(function): \(message)") #endif logs.append("\(message)") } } // MARK: - Tunnel Manager class TunnelManager: ObservableObject { @Published var tunnelStatus: TunnelStatus = .disconnected static var shared = TunnelManager() private var vpnManager: NETunnelProviderManager? private var tunnelDeviceIp: String { UserDefaults.standard.string(forKey: "TunnelDeviceIP") ?? "10.7.0.0" } private var tunnelFakeIp: String { UserDefaults.standard.string(forKey: "TunnelFakeIP") ?? "10.7.0.1" } private var tunnelSubnetMask: String { UserDefaults.standard.string(forKey: "TunnelSubnetMask") ?? "255.255.255.0" } private var tunnelBundleId: String { Bundle.main.bundleIdentifier!.appending(".TunnelProv") } enum TunnelStatus: String { case disconnected = "Disconnected" case connecting = "Connecting" case connected = "Connected" case disconnecting = "Disconnecting" case error = "Error" } private init() { loadTunnelPreferences() NotificationCenter.default.addObserver(self, selector: #selector(statusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil) } private func loadTunnelPreferences() { NETunnelProviderManager.loadAllFromPreferences { [weak self] (managers, error) in guard let self = self else { return } DispatchQueue.main.async { if let error = error { VPNLogger.shared.log("Error loading preferences: \(error.localizedDescription)") self.tunnelStatus = .error return } if let managers = managers, !managers.isEmpty { for manager in managers { if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol, proto.providerBundleIdentifier == self.tunnelBundleId { self.vpnManager = manager self.updateTunnelStatus(from: manager.connection.status) VPNLogger.shared.log("Loaded existing tunnel configuration") break } } if self.vpnManager == nil, let firstManager = managers.first { self.vpnManager = firstManager self.updateTunnelStatus(from: firstManager.connection.status) VPNLogger.shared.log("Using existing tunnel configuration") } } else { VPNLogger.shared.log("No existing tunnel configuration found") } } } } @objc private func statusDidChange(_ notification: Notification) { if let connection = notification.object as? NEVPNConnection { updateTunnelStatus(from: connection.status) } } private func updateTunnelStatus(from connectionStatus: NEVPNStatus) { DispatchQueue.main.async { switch connectionStatus { case .invalid, .disconnected: self.tunnelStatus = .disconnected case .connecting: self.tunnelStatus = .connecting case .connected: self.tunnelStatus = .connected case .disconnecting: self.tunnelStatus = .disconnecting case .reasserting: self.tunnelStatus = .connecting @unknown default: self.tunnelStatus = .error } VPNLogger.shared.log("VPN status updated: \(self.tunnelStatus.rawValue)") if connectionStatus == .connected && heartbeatStartPending { startHeartbeatInBackground() } } } private func createOrUpdateTunnelConfiguration(completion: @escaping (Bool) -> Void) { NETunnelProviderManager.loadAllFromPreferences { [weak self] (managers, error) in guard let self = self else { return completion(false) } if let error = error { VPNLogger.shared.log("Error loading preferences: \(error.localizedDescription)") return completion(false) } let manager: NETunnelProviderManager if let existingManagers = managers, !existingManagers.isEmpty { if let matchingManager = existingManagers.first(where: { ($0.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.tunnelBundleId }) { manager = matchingManager VPNLogger.shared.log("Updating existing tunnel configuration") } else { manager = existingManagers[0] VPNLogger.shared.log("Using first available tunnel configuration") } } else { manager = NETunnelProviderManager() VPNLogger.shared.log("Creating new tunnel configuration") } manager.localizedDescription = "StikDebug" let proto = NETunnelProviderProtocol() proto.providerBundleIdentifier = self.tunnelBundleId proto.serverAddress = "StikDebug's Local Network Tunnel" manager.protocolConfiguration = proto manager.isOnDemandEnabled = true manager.isEnabled = true manager.saveToPreferences { [weak self] error in guard let self = self else { return completion(false) } DispatchQueue.main.async { if let error = error { VPNLogger.shared.log("Error saving tunnel configuration: \(error.localizedDescription)") completion(false) return } self.vpnManager = manager VPNLogger.shared.log("Tunnel configuration saved successfully") completion(true) } } } } func startVPN() { if let manager = vpnManager { startExistingVPN(manager: manager) } else { createOrUpdateTunnelConfiguration { [weak self] success in guard let self = self, success else { return } self.loadTunnelPreferences() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if let manager = self.vpnManager { self.startExistingVPN(manager: manager) } } } } } private func startExistingVPN(manager: NETunnelProviderManager) { guard tunnelStatus == .disconnected || tunnelStatus == .error else { VPNLogger.shared.log("Ignoring VPN start; current status: \(tunnelStatus.rawValue)") return } tunnelStatus = .connecting let options: [String: NSObject] = [ "TunnelDeviceIP": tunnelDeviceIp as NSObject, "TunnelFakeIP": tunnelFakeIp as NSObject, "TunnelSubnetMask": tunnelSubnetMask as NSObject ] do { try manager.connection.startVPNTunnel(options: options) VPNLogger.shared.log("Network tunnel start initiated") } catch { tunnelStatus = .error VPNLogger.shared.log("Failed to start tunnel: \(error.localizedDescription)") } } func stopVPN() { guard let manager = vpnManager else { return } tunnelStatus = .disconnecting manager.connection.stopVPNTunnel() VPNLogger.shared.log("Network tunnel stop initiated") } } // MARK: - AccentColor Environment Key (leave available but unused) struct AccentColorKey: EnvironmentKey { static let defaultValue: Color = .accentColor } extension EnvironmentValues { var accentColor: Color { get { self[AccentColorKey.self] } set { self[AccentColorKey.self] = newValue } } } // MARK: - Helper Functions and Globals let fileManager = FileManager.default func httpGet(_ urlString: String, result: @escaping (String?) -> Void) { if let url = URL(string: urlString) { let task = URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { print("Error: \(error.localizedDescription)") result(nil) return } if let data = data, let httpResponse = response as? HTTPURLResponse { if httpResponse.statusCode == 200 { print("Response: \(httpResponse.statusCode)") if let dataString = String(data: data, encoding: .utf8) { result(dataString) } } else { print("Received non-200 status code: \(httpResponse.statusCode)") } } } task.resume() } } func UpdateRetrieval() -> Bool { var ver: String { let marketingVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" return marketingVersion } let urlString = "https://raw.githubusercontent.com/0-Blu/StikJIT/refs/heads/main/version.txt" var res = false httpGet(urlString) { result in if let fc = result { if ver != fc { res = true } } } return res } // MARK: - DNS Checker class DNSChecker: ObservableObject { @Published var appleIP: String? @Published var controlIP: String? @Published var dnsError: String? func checkDNS() { checkIfConnectedToWifi { [weak self] wifiConnected in guard let self = self else { return } if wifiConnected { let group = DispatchGroup() group.enter() self.lookupIPAddress(for: "gs.apple.com") { ip in DispatchQueue.main.async { self.appleIP = ip } group.leave() } group.enter() self.lookupIPAddress(for: "google.com") { ip in DispatchQueue.main.async { self.controlIP = ip } group.leave() } group.notify(queue: .main) { if self.controlIP == nil { self.dnsError = "No internet connection." print("Control host lookup failed, so no internet connection.") } else if self.appleIP == nil { self.dnsError = "Apple DNS blocked. Your network might be filtering Apple traffic." print("Control lookup succeeded, but Apple lookup failed: likely blocked.") } else { self.dnsError = nil print("DNS lookups succeeded: Apple -> \(self.appleIP!), Control -> \(self.controlIP!)") } } } else { DispatchQueue.main.async { self.dnsError = nil print("Not connected to WiFi; continuing without DNS check.") } } } } private func checkIfConnectedToWifi(completion: @escaping (Bool) -> Void) { let monitor = NWPathMonitor(requiredInterfaceType: .wifi) monitor.pathUpdateHandler = { path in completion(path.status == .satisfied) monitor.cancel() } let queue = DispatchQueue.global(qos: .background) monitor.start(queue: queue) } private func lookupIPAddress(for host: String, completion: @escaping (String?) -> Void) { DispatchQueue.global(qos: .background).async { var hints = addrinfo( ai_flags: 0, ai_family: AF_UNSPEC, ai_socktype: SOCK_STREAM, ai_protocol: 0, ai_addrlen: 0, ai_canonname: nil, ai_addr: nil, ai_next: nil ) var res: UnsafeMutablePointer? let err = getaddrinfo(host, nil, &hints, &res) if err != 0 { DispatchQueue.main.async { completion(nil) } return } var ipAddress: String? var ptr = res while ptr != nil { if let addr = ptr?.pointee.ai_addr { var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) if getnameinfo(addr, ptr!.pointee.ai_addrlen, &hostBuffer, socklen_t(hostBuffer.count), nil, 0, NI_NUMERICHOST) == 0 { ipAddress = String(cString: hostBuffer) break } } ptr = ptr?.pointee.ai_next } freeaddrinfo(res) DispatchQueue.main.async { completion(ipAddress) } } } } // MARK: - Main App // Global state variable for the heartbeat response. var pubHeartBeat = false private var heartbeatStartPending = false private var heartbeatStartInProgress = false @main struct HeartbeatApp: App { @AppStorage("hasLaunchedBefore") var hasLaunchedBefore: Bool = false @AppStorage("customAccentColor") private var customAccentColorHex: String = "" @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue @AppStorage("autoStartVPN") private var autoStartVPN = true @State private var showWelcomeSheet: Bool = false @State private var show_alert = false @State private var alert_string = "" @State private var alert_title = "" @StateObject private var mount = MountingProgress.shared @StateObject private var themeExpansionManager = ThemeExpansionManager() @Environment(\.scenePhase) private var scenePhase // Observe scene lifecycle let urls: [String] = [ "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist", "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg", "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache" ] let outputDir: String = "DDI" let outputFiles: [String] = [ "DDI/BuildManifest.plist", "DDI/Image.dmg", "DDI/Image.dmg.trustcache" ] init() { registerAdvancedOptionsDefault() ContinuedProcessingManager.shared.configureIfNeeded() newVerCheck() let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))! let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))! method_exchangeImplementations(origMethod, fixMethod) // Initialize UIKit tint from stored accent at launch (defaults to blue until entitlements load) HeartbeatApp.updateUIKitTint(customHex: customAccentColorHex, hasAccess: false) } // Make this static so we can call it without capturing self in init private static func updateUIKitTint(customHex: String, hasAccess: Bool) { let color: UIColor if hasAccess, !customHex.isEmpty, let swiftColor = Color(hex: customHex) { color = UIColor(swiftColor) } else { color = .systemBlue } UIView.appearance().tintColor = color } func newVerCheck() { let currentDate = Calendar.current.startOfDay(for: Date()) let VUA = UserDefaults.standard.object(forKey: "VersionUpdateAlert") as? Date ?? Date.distantPast if currentDate > Calendar.current.startOfDay(for: VUA) { if UpdateRetrieval() { alert_title = "Update Avaliable!" let urlString = "https://raw.githubusercontent.com/0-Blu/StikJIT/refs/heads/main/version.txt" httpGet(urlString) { result in if result == nil { return } alert_string = "Update to: version \(result!)!" show_alert = true } } UserDefaults.standard.set(currentDate, forKey: "VersionUpdateAlert") } } private func triggerAutoVPNStartIfNeeded() { guard autoStartVPN else { return } let manager = TunnelManager.shared if manager.tunnelStatus == .disconnected || manager.tunnelStatus == .error { manager.startVPN() } } private var globalAccent: Color { themeExpansionManager.resolvedAccentColor(from: customAccentColorHex) } var body: some Scene { WindowGroup { BackgroundContainer { MainTabView() .onAppear { let fileManager = FileManager.default for (index, urlString) in urls.enumerated() { let destinationURL = URL.documentsDirectory.appendingPathComponent(outputFiles[index]) if !fileManager.fileExists(atPath: destinationURL.path) { downloadFile(from: urlString, to: destinationURL) { result in if (result != "") { alert_title = "An Error has Occurred" alert_string = "[Download DDI Error]: " + result show_alert = true } } } } } .overlay( ZStack { if show_alert { CustomErrorView( title: alert_title, message: alert_string, onDismiss: { show_alert = false }, showButton: true, primaryButtonText: "OK" ) } } ) } .themeExpansionManager(themeExpansionManager) // Apply global tint to all SwiftUI views in this window .tint(globalAccent) .onAppear { // On first launch, present the welcome sheet. // Otherwise, start the VPN automatically. if !hasLaunchedBefore { showWelcomeSheet = true } else { triggerAutoVPNStartIfNeeded() } HeartbeatApp.updateUIKitTint(customHex: customAccentColorHex, hasAccess: themeExpansionManager.hasThemeExpansion) } .onChange(of: themeExpansionManager.hasThemeExpansion) { hasAccess in HeartbeatApp.updateUIKitTint(customHex: customAccentColorHex, hasAccess: hasAccess) } .onChange(of: customAccentColorHex) { newHex in HeartbeatApp.updateUIKitTint(customHex: newHex, hasAccess: themeExpansionManager.hasThemeExpansion) } .sheet(isPresented: $showWelcomeSheet) { WelcomeSheetView { // When the user taps "Continue", mark the app as launched and start the VPN if allowed. hasLaunchedBefore = true showWelcomeSheet = false triggerAutoVPNStartIfNeeded() } } } .onChange(of: scenePhase) { newPhase in if newPhase == .active { print("App became active – restarting heartbeat") startHeartbeatInBackground() } } } } // MARK: - Additional Helpers actor FunctionGuard { private var runningTask: Task? func execute(_ work: @escaping @Sendable () -> T) async -> T { if let task = runningTask { return await task.value } let task = Task.detached { work() } runningTask = task let result = await task.value runningTask = nil return result } } class MountingProgress: ObservableObject { static var shared = MountingProgress() @Published var mountProgress: Double = 0.0 @Published var mountingThread: Thread? @Published var coolisMounted: Bool = false func checkforMounted() { DispatchQueue.global(qos: .utility).async { let mounted = isMounted() DispatchQueue.main.async { self.coolisMounted = mounted } } } func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) { let percentage = Double(progress) / Double(total) * 100.0 print("Mounting progress: \(percentage)%") DispatchQueue.main.async { self.mountProgress = percentage } } func pubMount() { mount() } private func mount() { guard TunnelManager.shared.tunnelStatus == .connected else { DispatchQueue.main.async { self.coolisMounted = false self.mountingThread = nil } return } let currentlyMounted = isMounted() DispatchQueue.main.async { self.coolisMounted = currentlyMounted } let pairingpath = URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path if isPairing(), !currentlyMounted { if let mountingThread = mountingThread { mountingThread.cancel() self.mountingThread = nil } mountingThread = Thread { [weak self] in guard let self = self else { return } let mountResult = mountPersonalDDI( imagePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg").path, trustcachePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path, manifestPath: URL.documentsDirectory.appendingPathComponent("DDI/BuildManifest.plist").path, pairingFilePath: pairingpath ) DispatchQueue.main.async { if mountResult != 0 { showAlert(title: "Error", message: "An Error Occured when Mounting the DDI\nError Code: \(mountResult)", showOk: true, showTryAgain: true) { shouldTryAgain in if shouldTryAgain { self.mount() } } } else { self.coolisMounted = true self.checkforMounted() } self.mountingThread = nil } } mountingThread!.qualityOfService = .background mountingThread!.name = "mounting" mountingThread!.start() } } } func isPairing() -> Bool { let pairingpath = URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path var pairingFile: IdevicePairingFile? let err = idevice_pairing_file_read(pairingpath, &pairingFile) if let err { print("Failed to read pairing file: \(err.pointee.code)") if err.pointee.code == -9 { // InvalidHostID is -9 return false } return false } return true } func startHeartbeatInBackground(requireVPNConnection: Bool = true) { assert(Thread.isMainThread, "startHeartbeatInBackground must be called on the main thread") let pairingFileURL = URL.documentsDirectory.appendingPathComponent("pairingFile.plist") guard FileManager.default.fileExists(atPath: pairingFileURL.path) else { heartbeatStartPending = false return } let vpnConnected = TunnelManager.shared.tunnelStatus == .connected if requireVPNConnection && !vpnConnected { if !heartbeatStartPending { print("Heartbeat start deferred until VPN connects") } heartbeatStartPending = true return } guard !heartbeatStartInProgress else { return } heartbeatStartPending = false heartbeatStartInProgress = true let heartBeatThread = Thread { defer { DispatchQueue.main.async { heartbeatStartInProgress = false } } let completionHandler: @convention(block) (Int32, String?) -> Void = { result, message in if result == 0 { print("Heartbeat started successfully: \(message ?? "")") pubHeartBeat = true if FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path) { MountingProgress.shared.pubMount() } } else { print("Error: \(message ?? "") (Code: \(result))") DispatchQueue.main.async { if result == -9 { do { try FileManager.default.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist")) print("Removed invalid pairing file") } catch { print("Error removing invalid pairing file: \(error)") } showAlert( title: "Invalid Pairing File", message: "The pairing file is invalid or expired. Please select a new pairing file.", showOk: true, showTryAgain: false, primaryButtonText: "Select New File" ) { _ in NotificationCenter.default.post(name: NSNotification.Name("ShowPairingFilePicker"), object: nil) } } else { showAlert( title: "Heartbeat Error", message: "Failed to connect to Heartbeat (\(result)). Are you connected to WiFi or is Airplane Mode enabled? Cellular data isn’t supported. Please launch the app at least once with WiFi enabled. After that, you can switch to cellular data to turn on the VPN, and once the VPN is active you can use Airplane Mode.", showOk: false, showTryAgain: true ) { shouldTryAgain in if shouldTryAgain { DispatchQueue.main.async { startHeartbeatInBackground() } } } } } } } JITEnableContext.shared.startHeartbeat(completionHandler: completionHandler, logger: nil) } heartBeatThread.qualityOfService = .background heartBeatThread.name = "Heartbeat" heartBeatThread.start() } func checkVPNConnection(callback: @escaping (Bool, String?) -> Void) { let host = NWEndpoint.Host("10.7.0.1") let port = NWEndpoint.Port(rawValue: 62078)! let connection = NWConnection(host: host, port: port, using: .tcp) var timeoutWorkItem: DispatchWorkItem? timeoutWorkItem = DispatchWorkItem { [weak connection] in if connection?.state != .ready { connection?.cancel() DispatchQueue.main.async { if timeoutWorkItem?.isCancelled == false { callback(false, "[TIMEOUT] The loopback VPN is not connected. Try closing this app, turn it off and back on.") } } } } connection.stateUpdateHandler = { [weak connection] state in switch state { case .ready: timeoutWorkItem?.cancel() connection?.cancel() DispatchQueue.main.async { callback(true, nil) } case .failed(let error): timeoutWorkItem?.cancel() connection?.cancel() DispatchQueue.main.async { if error == NWError.posix(.ETIMEDOUT) { callback(false, "The loopback VPN is not connected. Try closing the app, turn it off and back on.") } else if error == NWError.posix(.ECONNREFUSED) { callback(false, "Wifi is not connected. StikJIT won't work on cellular data.") } else { callback(false, "em proxy check error: \(error.localizedDescription)") } } default: break } } connection.start(queue: .global()) if let workItem = timeoutWorkItem { DispatchQueue.global().asyncAfter(deadline: .now() + 20, execute: workItem) } } public func showAlert(title: String, message: String, showOk: Bool, showTryAgain: Bool = false, primaryButtonText: String? = nil, messageType: MessageType = .error, completion: ((Bool) -> Void)? = nil) { DispatchQueue.main.async { guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } let rootViewController = scene.windows.first?.rootViewController if showTryAgain { let customErrorView = CustomErrorView( title: title, message: message, onDismiss: { rootViewController?.presentedViewController?.dismiss(animated: true) completion?(false) }, showButton: true, primaryButtonText: primaryButtonText ?? "Try Again", onPrimaryButtonTap: { completion?(true) }, messageType: messageType ) let hostingController = UIHostingController(rootView: customErrorView) hostingController.modalPresentationStyle = .overFullScreen hostingController.modalTransitionStyle = .crossDissolve hostingController.view.backgroundColor = .clear rootViewController?.present(hostingController, animated: true) } else if showOk { let customErrorView = CustomErrorView( title: title, message: message, onDismiss: { rootViewController?.presentedViewController?.dismiss(animated: true) completion?(true) }, showButton: true, primaryButtonText: primaryButtonText ?? "OK", onPrimaryButtonTap: { rootViewController?.presentedViewController?.dismiss(animated: true) completion?(true) }, messageType: messageType ) let hostingController = UIHostingController(rootView: customErrorView) hostingController.modalPresentationStyle = .overFullScreen hostingController.modalTransitionStyle = .crossDissolve hostingController.view.backgroundColor = .clear rootViewController?.present(hostingController, animated: true) } else { let customErrorView = CustomErrorView( title: title, message: message, onDismiss: { rootViewController?.presentedViewController?.dismiss(animated: true) completion?(false) }, showButton: false, messageType: messageType ) let hostingController = UIHostingController(rootView: customErrorView) hostingController.modalPresentationStyle = .overFullScreen hostingController.modalTransitionStyle = .crossDissolve hostingController.view.backgroundColor = .clear rootViewController?.present(hostingController, animated: true) } } } func downloadFile(from urlString: String, to destinationURL: URL, completion: @escaping (String) -> Void) { let fileManager = FileManager.default let documentsDirectory = URL.documentsDirectory guard let url = URL(string: urlString) else { print("Invalid URL: \(urlString)") completion("[Internal Invalid URL error]") return } let task = URLSession.shared.downloadTask(with: url) { (tempLocalUrl, response, error) in guard let tempLocalUrl = tempLocalUrl, error == nil else { print("Error downloading file from \(urlString): \(String(describing: error))") completion("Are you connected to the internet? [Download Failed]") return } do { try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) try fileManager.moveItem(at: tempLocalUrl, to: destinationURL) print("Downloaded \(urlString) to \(destinationURL.path)") } catch { print("Error saving file: \(error)") } } task.resume() completion("") } func redownloadDDI() { let fileManager = FileManager.default let ddiFiles = [ "DDI/BuildManifest.plist", "DDI/Image.dmg", "DDI/Image.dmg.trustcache" ] for file in ddiFiles { let fileURL = URL.documentsDirectory.appendingPathComponent(file) if fileManager.fileExists(atPath: fileURL.path) { do { try fileManager.removeItem(at: fileURL) print("Deleted \(file)") } catch { print("Error deleting \(file): \(error)") } } } let urls = [ "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist", "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg", "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache" ] let group = DispatchGroup() for (index, urlString) in urls.enumerated() { let destinationURL = URL.documentsDirectory.appendingPathComponent(ddiFiles[index]) group.enter() downloadFile(from: urlString, to: destinationURL) { result in if (result != "") { showAlert(title: "An Error has Occurred", message: "[Download DDI Error]: " + result, showOk: true) } group.leave() } } group.notify(queue: .main) { showAlert(title: "Success", message: "DDI files have been redownloaded.", showOk: true) } }