// // ContentView.swift // StikJIT // // Created by Stephen on 3/26/25. // import SwiftUI import UniformTypeIdentifiers import Pipify import UIKit import WidgetKit struct JITEnableConfiguration { var bundleID: String? = nil var pid : Int? = nil var scriptData: Data? = nil var scriptName : String? = nil } struct HomeView: View { @AppStorage("username") private var username = "User" @AppStorage("customAccentColor") private var customAccentColorHex: String = "" @Environment(\.colorScheme) private var colorScheme @Environment(\.accentColor) private var environmentAccentColor let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @AppStorage("bundleID") private var bundleID: String = "" @AppStorage("recentApps") private var recentApps: [String] = [] @AppStorage("favoriteApps") private var favoriteApps: [String] = [] @State private var isProcessing = false @State private var isShowingInstalledApps = false @State private var isShowingPairingFilePicker = false @State private var pairingFileExists: Bool = false @State private var pairingFilePresentOnDisk: Bool = false @State private var isValidatingPairingFile = false @State private var lastValidatedPairingSignature: PairingFileSignature? = nil @State private var showPairingFileMessage = false @State private var pairingFileIsValid = false @State private var isImportingFile = false @State private var importProgress: Float = 0.0 @State private var showPIDSheet = false @AppStorage("recentPIDs") private var recentPIDs: [Int] = [] @State private var justCopied = false @State private var showingConsoleLogsView = false @State private var viewDidAppeared = false @State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil @AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false @AppStorage("useDefaultScript") private var useDefaultScript = false @AppStorage("enablePiP") private var enablePiP = true @State var scriptViewShow = false @State var pipRequired = false @AppStorage("DefaultScriptName") var selectedScript = "attachDetach.js" @State var jsModel: RunJSViewModel? @StateObject private var tunnel = TunnelManager.shared @State private var heartbeatOK = false @State private var cachedAppNames: [String: String] = [:] @State private var isLoadingQuickApps = false @AppStorage("pinnedSystemApps") private var pinnedSystemApps: [String] = [] @AppStorage("pinnedSystemAppNames") private var pinnedSystemAppNames: [String: String] = [:] @State private var launchingSystemApps: Set = [] @State private var systemLaunchMessage: String? = nil @State private var developerModeStatus: DeveloperModeStatus = .unknown @State private var isCheckingDeveloperMode = false @State private var lastDeveloperModeCheck: Date? = nil @State private var developerModeCheckToken = UUID() @AppStorage("showiOS26Disclaimer") private var showiOS26Disclaimer: Bool = true @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue @Environment(\.themeExpansionManager) private var themeExpansion private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle } private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) } private var accentColor: Color { themeExpansion?.resolvedAccentColor(from: customAccentColorHex) ?? .blue } private var ddiMounted: Bool { isMounted() } private var canConnectByApp: Bool { pairingFileExists && ddiMounted } private let pairingFileURL = URL.documentsDirectory.appendingPathComponent("pairingFile.plist") private var isOnOrAfteriOS26: Bool { let v = ProcessInfo.processInfo.operatingSystemVersion return v.majorVersion >= 26 } var body: some View { NavigationStack { ZStack { ThemedBackground(style: backgroundStyle) .ignoresSafeArea() ScrollView { VStack(spacing: 20) { if isOnOrAfteriOS26 || showiOS26Disclaimer { disclaimerCard .transition(.opacity.combined(with: .move(edge: .top))) } readinessCard // if pairingFileExists { // quickConnectCard // } if !pinnedLaunchItems.isEmpty { launchShortcutsCard } toolsCard tipsCard } .padding(.horizontal, 20) .padding(.vertical, 30) } if isImportingFile { Color.black.opacity(0.35).ignoresSafeArea() ProgressView("Processing pairing file…") .padding(16) .background( RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder(Color.white.opacity(0.15), lineWidth: 1) ) ) .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4) } if showPairingFileMessage && pairingFileIsValid && !isImportingFile { toast("✓ Pairing file successfully imported") } if justCopied { toast("Copied") } if let message = systemLaunchMessage { toast(message) } } .navigationTitle("Home") } .preferredColorScheme(preferredScheme) .onAppear { checkPairingFileExists() refreshBackground() loadAppListIfNeeded() NotificationCenter.default.addObserver( forName: NSNotification.Name("ShowPairingFilePicker"), object: nil, queue: .main ) { _ in isShowingPairingFilePicker = true } } .onReceive(timer) { _ in refreshBackground() checkPairingFileExists() heartbeatOK = pubHeartBeat } .onChange(of: pairingFileExists) { _, newValue in if newValue { loadAppListIfNeeded(force: cachedAppNames.isEmpty) developerModeStatus = .unknown lastDeveloperModeCheck = nil refreshBackground() } else { cachedAppNames = [:] developerModeStatus = .unknown lastDeveloperModeCheck = nil developerModeCheckToken = UUID() isCheckingDeveloperMode = false } } .onChange(of: favoriteApps) { _, _ in loadAppListIfNeeded() } .onChange(of: recentApps) { _, _ in loadAppListIfNeeded() } .fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) { result in switch result { case .success(let url): let fileManager = FileManager.default let accessing = url.startAccessingSecurityScopedResource() if fileManager.fileExists(atPath: url.path) { do { let dest = URL.documentsDirectory.appendingPathComponent("pairingFile.plist") if FileManager.default.fileExists(atPath: dest.path) { try fileManager.removeItem(at: dest) } try fileManager.copyItem(at: url, to: dest) DispatchQueue.main.async { isImportingFile = true importProgress = 0 pairingFileExists = true } startHeartbeatInBackground() let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { t in DispatchQueue.main.async { if importProgress < 1 { importProgress += 0.25 } else { t.invalidate() isImportingFile = false pairingFileIsValid = true withAnimation { showPairingFileMessage = true } DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { withAnimation { showPairingFileMessage = false } } } } } RunLoop.current.add(progressTimer, forMode: .common) } catch { print("Error copying file: \(error)") } } if accessing { url.stopAccessingSecurityScopedResource() } case .failure(let error): print("Failed to import file: \(error)") } } .sheet(isPresented: $showingConsoleLogsView) { if let manager = themeExpansion { ConsoleLogsView().themeExpansionManager(manager) } else { ConsoleLogsView() } } .sheet(isPresented: $isShowingInstalledApps) { InstalledAppsListView { selectedBundle in bundleID = selectedBundle isShowingInstalledApps = false HapticFeedbackHelper.trigger() var autoScriptData: Data? = nil var autoScriptName: String? = nil let appName: String? = (try? JITEnableContext.shared.getAppList()[selectedBundle]) if #available(iOS 26, *) { if ProcessInfo.processInfo.hasTXM, let appName { if appName == "maciOS" { if let url = Bundle.main.url(forResource: "script1", withExtension: "js"), let data = try? Data(contentsOf: url) { autoScriptData = data autoScriptName = "script1.js" } } else if appName == "Amethyst" { if let url = Bundle.main.url(forResource: "script2", withExtension: "js"), let data = try? Data(contentsOf: url) { autoScriptData = data autoScriptName = "script2.js" } } else if appName == "MeloNX" { if let url = Bundle.main.url(forResource: "melo", withExtension: "js"), let data = try? Data(contentsOf: url) { autoScriptData = data autoScriptName = "melo.js" } } else if appName == "UTM" || appName == "DolphiniOS" { if let url = Bundle.main.url(forResource: "utmjit", withExtension: "js"), let data = try? Data(contentsOf: url) { autoScriptData = data autoScriptName = "utmjit.js" } } } } startJITInBackground(bundleID: selectedBundle, pid: nil, scriptData: autoScriptData, scriptName: autoScriptName, triggeredByURLScheme: false) } } .pipify(isPresented: Binding( get: { pipRequired && enablePiP }, set: { newValue in pipRequired = newValue } )) { RunJSViewPiP(model: $jsModel) } .sheet(isPresented: $scriptViewShow) { NavigationView { if let jsModel { RunJSView(model: jsModel) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Done") { scriptViewShow = false } } } .navigationTitle(selectedScript) .navigationBarTitleDisplayMode(.inline) } } } .sheet(isPresented: $showPIDSheet) { ConnectByPIDSheet( recentPIDs: $recentPIDs, onPasteCopyToast: { showCopiedToast() }, onConnect: { pid in HapticFeedbackHelper.trigger() startJITInBackground(pid: pid) } ) } .onOpenURL { url in guard let host = url.host else { return } let components = URLComponents(url: url, resolvingAgainstBaseURL: false) switch host { case "enable-jit": var config = JITEnableConfiguration() if let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value, let pid = Int(pidStr) { config.pid = pid } if let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value { config.bundleID = bundleId } if let scriptBase64URL = components?.queryItems?.first(where: { $0.name == "script-data" })?.value?.removingPercentEncoding { let base64 = base64URLToBase64(scriptBase64URL) if let scriptData = Data(base64Encoded: base64) { config.scriptData = scriptData } } if let scriptName = components?.queryItems?.first(where: { $0.name == "script-name" })?.value { config.scriptName = scriptName } if viewDidAppeared { startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true) } else { pendingJITEnableConfiguration = config } case "launch-app": if let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value { HapticFeedbackHelper.trigger() DispatchQueue.global(qos: .userInitiated).async { let success = JITEnableContext.shared.launchAppWithoutDebug(bundleId, logger: nil) DispatchQueue.main.async { let nameRaw = pinnedSystemAppNames[bundleId] ?? friendlyName(for: bundleId) let name = shortDisplayName(from: nameRaw) systemLaunchMessage = success ? String(format: "Launch requested: %@".localized, name) : String(format: "Failed to launch %@".localized, name) scheduleSystemToastDismiss() } } } default: break } } .onAppear { viewDidAppeared = true if let config = pendingJITEnableConfiguration { startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true) pendingJITEnableConfiguration = nil } } } // MARK: - Styled Sections private var readinessCard: some View { homeCard { VStack(alignment: .leading, spacing: 16) { let summary = readinessSummary HStack(alignment: .top, spacing: 12) { StatusGlyph(icon: summary.icon, tint: summary.tint) VStack(alignment: .leading, spacing: 6) { Text("Welcome, \(username)") .font(.system(.title2, design: .rounded).weight(.bold)) .foregroundStyle(.primary) Text(summary.title) .font(.headline.weight(.semibold)) .foregroundStyle(summary.tint) Text(summary.subtitle) .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } } VStack(alignment: .leading, spacing: 12) { ForEach(readinessChecklist) { item in ChecklistRow(item: item, accentColor: accentColor) } } VStack(spacing: 10) { Button(action: primaryActionTapped) { whiteCardButtonLabel( icon: primaryActionIcon, title: primaryActionTitle, isLoading: isProcessing || isValidatingPairingFile ) } .disabled(isProcessing || isValidatingPairingFile) if pairingFileExists && enableAdvancedOptions { Button(action: { showPIDSheet = true }) { secondaryButtonLabel(icon: "number.circle", title: "Connect by PID") } .disabled(isProcessing) } } if isImportingFile { pairingImportProgressView } else if showPairingFileMessage && pairingFileIsValid { pairingSuccessMessage } } } } private var readinessSummary: ReadinessSummary { if isValidatingPairingFile { return .init( title: "Validating pairing file", subtitle: "Hang tight while we verify the pairing file from your trusted computer.", icon: "hourglass.circle.fill", tint: .orange ) } if !pairingFileExists { if pairingFilePresentOnDisk { return .init( title: "Pairing file needs attention", subtitle: "We found a pairing file but couldn’t read it.", icon: "doc.badge.exclamationmark", tint: .yellow ) } return .init( title: "Import your pairing file", subtitle: "", icon: "doc.badge.plus", tint: .orange ) } switch developerModeStatus { case .unknown, .checking: return .init( title: "Checking Developer Mode", subtitle: "Keep your device unlocked while we verify Developer Mode is enabled.", icon: "gearshape", tint: .orange ) case .disabled: return .init( title: "Enable Developer Mode", subtitle: "Turn on Developer Mode in Settings > Privacy & Security, then restart if iOS asks you to.", icon: "gearshape.fill", tint: .orange ) case .error: return .init( title: "Can’t confirm Developer Mode", subtitle: "Unlock your device and make sure it’s connected. We’ll try again automatically.", icon: "questionmark.app.fill", tint: .yellow ) case .enabled: break } if !ddiMounted { return .init( title: "Mount the Developer Disk Image", subtitle: "", icon: "externaldrive.badge.exclamationmark", tint: .yellow ) } if tunnel.tunnelStatus != .connected { return .init( title: "Connect the VPN", subtitle: "Tap Allow when iOS prompts you, then flip the toggle in Settings if needed.", icon: "lock.slash", tint: .orange ) } if !heartbeatOK { return .init( title: "Waiting for heartbeat", subtitle: "StikDebug is reaching out to your device. We’ll connect automatically once it responds.", icon: "waveform.path.ecg", tint: .orange ) } return .init( title: "Ready when you are", subtitle: "Everything is ready. Select an app to start debugging in seconds.", icon: "bolt.horizontal.circle.fill", tint: .green ) } private var primaryActionTitle: String { if isValidatingPairingFile { return "Validating…" } if !pairingFileExists { return pairingFilePresentOnDisk ? "Re-import Pairing File" : "Import Pairing File" } switch developerModeStatus { case .unknown, .checking: return "Checking Developer Mode…" case .disabled: return "Enable Developer Mode" case .error: return "Retry Developer Mode Check" case .enabled: break } if !ddiMounted { return "Mount Developer Disk Image" } return "Connect by App" } private var primaryActionIcon: String { if isValidatingPairingFile { return "hourglass" } if !pairingFileExists { return pairingFilePresentOnDisk ? "arrow.clockwise" : "doc.badge.plus" } switch developerModeStatus { case .unknown, .checking: return "hourglass" case .disabled: return "exclamationmark.triangle" case .error: return "questionmark.app.fill" case .enabled: break } if !ddiMounted { return "externaldrive" } return "cable.connector.horizontal" } private var readinessChecklist: [ChecklistItem] { let vpnConnected = tunnel.tunnelStatus == .connected let pairingItem: ChecklistItem if isValidatingPairingFile { pairingItem = ChecklistItem( title: "Pairing file", subtitle: "Validating pairing file…", status: .waiting, actionTitle: nil, action: nil ) } else if pairingFileExists { pairingItem = ChecklistItem( title: "Pairing file", subtitle: "Imported and valid.", status: .ready, actionTitle: nil, action: nil ) } else if pairingFilePresentOnDisk { pairingItem = ChecklistItem( title: "Pairing file", subtitle: "We couldn’t read the pairing file that’s on disk. Re-import it from your trusted computer.", status: .attention, actionTitle: "Re-import", action: { isShowingPairingFilePicker = true } ) } else { pairingItem = ChecklistItem( title: "Pairing file", subtitle: "Import the pairing file generated from your trusted computer.", status: .actionRequired, actionTitle: "Import", action: { isShowingPairingFilePicker = true } ) } let developerModeItem: ChecklistItem = { if !pairingFileExists { return ChecklistItem( title: "Developer Mode", subtitle: "Import your pairing file to check the Developer Mode status.", status: .waiting, actionTitle: nil, action: nil ) } switch developerModeStatus { case .unknown, .checking: return ChecklistItem( title: "Developer Mode", subtitle: "Checking developer mode status…", status: .waiting, actionTitle: nil, action: nil ) case .enabled: return ChecklistItem( title: "Developer Mode", subtitle: "Enabled.", status: .ready, actionTitle: nil, action: nil ) case .disabled: return ChecklistItem( title: "Developer Mode", subtitle: "Turn on Developer Mode in Settings > Privacy & Security.", status: .actionRequired, actionTitle: nil, action: nil ) case .error: return ChecklistItem( title: "Developer Mode", subtitle: "We couldn’t determine the status. Unlock your device and keep it connected.", status: .attention, actionTitle: nil, action: nil ) } }() return [ pairingItem, developerModeItem, ChecklistItem( title: "Developer Disk Image", subtitle: ddiMounted ? "Mounted successfully." : "", status: ddiMounted ? .ready : .attention, actionTitle: nil, action: nil ), ChecklistItem( title: "VPN tunnel", subtitle: vpnConnected ? "Active." : "Connect the StikDebug VPN from the Settings app if it’s not already on.", status: vpnConnected ? .ready : .attention, actionTitle: nil, action: nil ), ChecklistItem( title: "Heartbeat", subtitle: heartbeatOK ? "Active." : "We retry automatically—leave the app open for a moment.", status: heartbeatOK ? .ready : .waiting, actionTitle: nil, action: nil ) ] } private var pairingImportProgressView: some View { VStack(spacing: 8) { HStack { Text("Processing pairing file…") .font(.system(.caption, design: .rounded)) .foregroundStyle(.secondary) Spacer() Text("\(Int(importProgress * 100))%") .font(.system(.caption, design: .rounded)) .foregroundStyle(.secondary) } GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 6, style: .continuous) .fill(Color(UIColor.tertiarySystemFill)) .frame(height: 8) RoundedRectangle(cornerRadius: 6, style: .continuous) .fill(accentColor) .frame(width: geo.size.width * CGFloat(importProgress), height: 8) .animation(.linear(duration: 0.25), value: importProgress) } } .frame(height: 8) } .accessibilityElement(children: .combine) } private var pairingSuccessMessage: some View { HStack(spacing: 10) { StatusDot(color: .green) Text("Pairing file successfully imported") .font(.system(.callout, design: .rounded)) .foregroundStyle(.green) Spacer(minLength: 0) } .padding(.top, 4) .transition(.opacity) } private func whiteCardButtonLabel(icon: String, title: String, isLoading: Bool = false) -> some View { HStack(spacing: 10) { if isLoading { ProgressView() .progressViewStyle(.circular) .tint(accentColor.contrastText()) .frame(width: 20, height: 20) } else { Image(systemName: icon) .font(.system(size: 20, weight: .semibold, design: .rounded)) } Text(title) .font(.system(.title3, design: .rounded).weight(.semibold)) } .frame(maxWidth: .infinity) .padding(.vertical, 14) .background(accentColor, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) .foregroundColor(accentColor.contrastText()) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.2), lineWidth: 0.5) ) .animation(.easeInOut(duration: 0.2), value: isLoading) } private func secondaryButtonLabel(icon: String, title: String) -> some View { HStack { Image(systemName: icon) .font(.system(size: 18, weight: .semibold, design: .rounded)) Text(title) .font(.system(.title3, design: .rounded).weight(.semibold)) } .frame(maxWidth: .infinity) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(Color(UIColor.secondarySystemBackground).opacity(0.6)) ) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.12), lineWidth: 1) ) .foregroundStyle(.primary) } private var quickConnectCard: some View { homeCard { VStack(alignment: .leading, spacing: 14) { HStack(spacing: 8) { Text("Quick Connect") .font(.headline.weight(.semibold)) .foregroundStyle(.primary) if isLoadingQuickApps { ProgressView() .progressViewStyle(.circular) .controlSize(.small) } } Text("Favorites and recents stay within reach so you can enable debug with ease.") .font(.footnote) .foregroundStyle(.secondary) if quickConnectItems.isEmpty { VStack(alignment: .leading, spacing: 10) { Text("Pin apps from the Installed Apps list to see them here.") .font(.caption) .foregroundStyle(.secondary) Button { isShowingInstalledApps = true } label: { secondaryButtonLabel(icon: "star", title: "Choose Favorites") } .buttonStyle(.plain) } } else { VStack(spacing: 10) { ForEach(quickConnectItems) { item in QuickConnectRow( item: item, accentColor: accentColor, isEnabled: canConnectByApp && !isProcessing, action: { HapticFeedbackHelper.trigger() startJITInBackground(bundleID: item.bundleID, pid: nil, scriptData: nil, scriptName: nil, triggeredByURLScheme: false) } ) } } } if !canConnectByApp { Text("Finish the pairing and mounting steps above to enable quick launches.") .font(.caption) .foregroundStyle(.secondary) } } } } private var launchShortcutsCard: some View { homeCard { VStack(alignment: .leading, spacing: 14) { Text("Launch Shortcuts".localized) .font(.headline.weight(.semibold)) .foregroundStyle(.primary) Text("Pin any app from Installed Apps and launch it here with ease.".localized) .font(.footnote) .foregroundStyle(.secondary) VStack(spacing: 10) { ForEach(pinnedLaunchItems) { item in SystemPinnedRow( item: item, accentColor: accentColor, isLaunching: launchingSystemApps.contains(item.bundleID), action: { launchSystemApp(item: item) }, onRemove: { removePinnedSystemApp(bundleID: item.bundleID) } ) } } } } } private var quickConnectItems: [QuickConnectItem] { var seen = Set() var ordered: [QuickConnectItem] = [] for bundle in favoriteApps + recentApps { guard seen.insert(bundle).inserted else { continue } ordered.append(QuickConnectItem(bundleID: bundle, displayName: friendlyName(for: bundle))) if ordered.count >= 4 { break } } return ordered } private var pinnedLaunchItems: [SystemPinnedItem] { pinnedSystemApps.compactMap { bundleID in let raw = pinnedSystemAppNames[bundleID] ?? friendlyName(for: bundleID) let displayName = shortDisplayName(from: raw) return SystemPinnedItem(bundleID: bundleID, displayName: displayName) } } // Prefer CoreDevice-reported app name, trimmed to a Home Screen–style label; else fall back to bundle ID last component. private func friendlyName(for bundleID: String) -> String { if let cached = cachedAppNames[bundleID], !cached.isEmpty { return shortDisplayName(from: cached) } let components = bundleID.split(separator: ".") if let last = components.last { let cleaned = last.replacingOccurrences(of: "_", with: " ") let trimmed = cleaned.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { return trimmed.capitalized } } return bundleID } // Heuristic “Home Screen” shortener for long marketing names. private func shortDisplayName(from name: String) -> String { var s = name // Keep only the part before common separators/subtitles. let separators = [" — ", " – ", " - ", ":", "|", "·", "•"] for sep in separators { if let r = s.range(of: sep) { s = String(s[..(@ViewBuilder content: () -> Content) -> some View { content() .padding(20) .background( RoundedRectangle(cornerRadius: 20, style: .continuous) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 20, style: .continuous) .strokeBorder(Color.white.opacity(0.15), lineWidth: 1) ) ) .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4) } private var toolsCard: some View { homeCard { VStack(alignment: .leading, spacing: 12) { Text("Tools") .font(.headline) .foregroundStyle(.secondary) VStack(spacing: 10) { Button { showingConsoleLogsView = true } label: { whiteCardButtonLabel(icon: "terminal", title: "Open Console") } // Button { // isShowingInstalledApps = true // } label: { // secondaryButtonLabel(icon: "list.bullet", title: "Installed Apps") // } // .buttonStyle(.plain) } } } } private var tipsCard: some View { homeCard { VStack(alignment: .leading, spacing: 12) { Text("Tips") .font(.headline) .foregroundStyle(.secondary) if !pairingFileExists { tipRow(systemImage: "doc.badge.plus", title: "Pairing file required", message: "Import your device’s pairing file to begin.") } if pairingFileExists && !ddiMounted { tipRow(systemImage: "externaldrive.badge.exclamationmark", title: "Developer Disk Image not mounted", message: "Go to Settings → Developer Disk Image and ensure it’s mounted.") } tipRow(systemImage: "lock.shield", title: "Local only", message: "StikDebug runs entirely on-device. No data leaves your device.") Divider().background(Color.white.opacity(0.1)) Button { if let url = URL(string: "https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md") { UIApplication.shared.open(url) } } label: { HStack(alignment: .center, spacing: 12) { Image(systemName: "questionmark.circle") .foregroundStyle(accentColor) .font(.system(size: 18, weight: .semibold)) VStack(alignment: .leading, spacing: 2) { Text("Pairing File Guide") .font(.subheadline.weight(.semibold)) Text("Step-by-step instructions from the community wiki.") .font(.footnote) .foregroundStyle(.secondary) } Spacer() Image(systemName: "chevron.right") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(.secondary) } .padding(.vertical, 4) } .buttonStyle(.plain) } } } private func tipRow(systemImage: String, title: String, message: String) -> some View { HStack(alignment: .top, spacing: 12) { Image(systemName: systemImage) .foregroundStyle(accentColor) .font(.system(size: 18, weight: .semibold)) VStack(alignment: .leading, spacing: 2) { Text(title) .font(.subheadline.weight(.semibold)) Text(message) .font(.footnote) .foregroundStyle(.secondary) } Spacer(minLength: 0) } .padding(.vertical, 4) } private func primaryActionTapped() { guard !isValidatingPairingFile else { return } if pairingFileExists { switch developerModeStatus { case .unknown, .checking: lastDeveloperModeCheck = nil refreshBackground() return case .disabled: lastDeveloperModeCheck = nil refreshBackground() showAlert( title: "Developer Mode Required".localized, message: "Turn on Developer Mode in Settings > Privacy & Security, then restart if prompted.".localized, showOk: true ) return case .error: lastDeveloperModeCheck = nil refreshBackground() showAlert( title: "Unable to Verify Developer Mode".localized, message: "Unlock your device and make sure it’s connected to the network, then try again.".localized, showOk: true ) return case .enabled: break } if !ddiMounted { showAlert(title: "Device Not Mounted".localized, message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.".localized, showOk: true) { _ in } return } isShowingInstalledApps = true } else { isShowingPairingFilePicker = true } } private func showCopiedToast() { withAnimation { justCopied = true } DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { withAnimation { justCopied = false } } } @ViewBuilder private func toast(_ text: String) -> some View { VStack { Spacer() Text(text) .font(.footnote.weight(.semibold)) .padding(.horizontal, 14) .padding(.vertical, 10) .background(.ultraThinMaterial, in: Capsule()) .overlay(Capsule().strokeBorder(Color.white.opacity(0.15), lineWidth: 1)) .shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 3) .transition(.move(edge: .bottom).combined(with: .opacity)) .padding(.bottom, 30) } .animation(.easeInOut(duration: 0.25), value: text) } private func checkPairingFileExists() { let fileExists = FileManager.default.fileExists(atPath: pairingFileURL.path) pairingFilePresentOnDisk = fileExists guard fileExists else { pairingFileExists = false lastValidatedPairingSignature = nil isValidatingPairingFile = false return } let signature = pairingFileSignature(for: pairingFileURL) guard needsValidation(for: signature) else { return } guard !isValidatingPairingFile else { return } isValidatingPairingFile = true DispatchQueue.global(qos: .utility).async { let valid = isPairing() DispatchQueue.main.async { pairingFileExists = valid lastValidatedPairingSignature = signature isValidatingPairingFile = false } } } private func needsValidation(for signature: PairingFileSignature) -> Bool { guard let lastSignature = lastValidatedPairingSignature else { return true } return lastSignature != signature } private func pairingFileSignature(for url: URL) -> PairingFileSignature { let attributes = (try? FileManager.default.attributesOfItem(atPath: url.path)) ?? [:] let modificationDate = attributes[.modificationDate] as? Date let sizeValue = (attributes[.size] as? NSNumber)?.uint64Value ?? 0 return PairingFileSignature(modificationDate: modificationDate, fileSize: sizeValue) } private func refreshBackground() { guard pairingFileExists else { developerModeStatus = .unknown lastDeveloperModeCheck = nil developerModeCheckToken = UUID() isCheckingDeveloperMode = false return } if isCheckingDeveloperMode { return } let minimumInterval: TimeInterval = developerModeStatus == .enabled ? 15 : 5 if let last = lastDeveloperModeCheck, Date().timeIntervalSince(last) < minimumInterval { return } isCheckingDeveloperMode = true let checkToken = UUID() developerModeCheckToken = checkToken if developerModeStatus != .enabled { developerModeStatus = .checking } DispatchQueue.global(qos: .utility).async { let result = queryDeveloperModeStatus() DispatchQueue.main.async { guard developerModeCheckToken == checkToken else { isCheckingDeveloperMode = false return } switch result { case .enabled: developerModeStatus = .enabled case .disabled: developerModeStatus = .disabled case .unavailable: developerModeStatus = .error } lastDeveloperModeCheck = Date() isCheckingDeveloperMode = false } } } private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback { return { pid, debugProxyHandle, semaphore in jsModel = RunJSViewModel(pid: Int(pid), debugProxy: debugProxyHandle, semaphore: semaphore) scriptViewShow = true DispatchQueue.global(qos: .background).async { do { try jsModel?.runScript(data: script, name: name) } catch { showAlert(title: "Error Occurred While Executing the Default Script.".localized, message: error.localizedDescription, showOk: true) } } } } private func startJITInBackground(bundleID: String? = nil, pid : Int? = nil, scriptData: Data? = nil, scriptName: String? = nil, triggeredByURLScheme: Bool = false) { isProcessing = true LogManager.shared.addInfoLog("Starting Debug for \(bundleID ?? String(pid ?? 0))") DispatchQueue.global(qos: .background).async { var scriptData = scriptData var scriptName = scriptName if enableAdvancedOptions && scriptData == nil { if scriptName == nil, let bundleID, let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String] { scriptName = mapping[bundleID] } if useDefaultScript && scriptName == nil { scriptName = selectedScript } if scriptData == nil, let scriptName { let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("scripts").appendingPathComponent(scriptName) if FileManager.default.fileExists(atPath: url.path) { do { scriptData = try Data(contentsOf: url) } catch { print("script load error: \(error)") } } } } else { // keep passed-in auto script if provided; otherwise nil } var callback: DebugAppCallback? = nil if ProcessInfo.processInfo.hasTXM, let sd = scriptData { callback = getJsCallback(sd, name: scriptName ?? bundleID ?? "Script") if triggeredByURLScheme { usleep(500000) } pipRequired = true } else { pipRequired = false } let logger: LogFunc = { message in if let message { LogManager.shared.addInfoLog(message) } } var success: Bool if let pid { success = JITEnableContext.shared.debugApp(withPID: Int32(pid), logger: logger, jsCallback: callback) if success { DispatchQueue.main.async { addRecentPID(pid) } } } else if let bundleID { success = JITEnableContext.shared.debugApp(withBundleID: bundleID, logger: logger, jsCallback: callback) } else { DispatchQueue.main.async { showAlert(title: "Failed to Debug App".localized, message: "Either bundle ID or PID should be specified.".localized, showOk: true) } success = false } if success { DispatchQueue.main.async { LogManager.shared.addInfoLog("Debug process completed for \(bundleID ?? String(pid ?? 0))") } } isProcessing = false pipRequired = false } } private func launchSystemApp(item: SystemPinnedItem) { guard !launchingSystemApps.contains(item.bundleID) else { return } launchingSystemApps.insert(item.bundleID) HapticFeedbackHelper.trigger() DispatchQueue.global(qos: .userInitiated).async { let success = JITEnableContext.shared.launchAppWithoutDebug(item.bundleID, logger: nil) DispatchQueue.main.async { launchingSystemApps.remove(item.bundleID) if success { LogManager.shared.addInfoLog("Launch request sent for \(item.bundleID)") systemLaunchMessage = String(format: "Launch requested: %@".localized, item.displayName) } else { LogManager.shared.addErrorLog("Failed to launch \(item.bundleID)") systemLaunchMessage = String(format: "Failed to launch %@".localized, item.displayName) } scheduleSystemToastDismiss() } } } private func removePinnedSystemApp(bundleID: String) { Haptics.light() pinnedSystemApps.removeAll { $0 == bundleID } pinnedSystemAppNames.removeValue(forKey: bundleID) persistPinnedSystemApps() } private func scheduleSystemToastDismiss() { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { if systemLaunchMessage != nil { withAnimation { systemLaunchMessage = nil } } } } private func persistPinnedSystemApps() { if let sharedDefaults = UserDefaults(suiteName: "group.com.stik.sj") { sharedDefaults.set(pinnedSystemApps, forKey: "pinnedSystemApps") sharedDefaults.set(pinnedSystemAppNames, forKey: "pinnedSystemAppNames") } WidgetCenter.shared.reloadAllTimelines() } private func addRecentPID(_ pid: Int) { var list = recentPIDs.filter { $0 != pid } list.insert(pid, at: 0) if list.count > 8 { list = Array(list.prefix(8)) } recentPIDs = list } func base64URLToBase64(_ base64url: String) -> String { var base64 = base64url.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") let pad = 4 - (base64.count % 4) if pad < 4 { base64 += String(repeating: "=", count: pad) } return base64 } // MARK: - iOS 26+ Disclaimer Card (above main card) private var disclaimerCard: some View { HStack(alignment: .top, spacing: 12) { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.yellow) .imageScale(.large) VStack(alignment: .leading, spacing: 4) { Text("Important for iOS 26+") .font(.subheadline.weight(.semibold)) .foregroundColor(.primary) Text("Limited compatibility on iOS 26 and later. Some apps may not function as expected yet. We’re actively improving support over time.") .font(.footnote) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } Spacer() if !isOnOrAfteriOS26 { Button { withAnimation { showiOS26Disclaimer = false } } label: { Image(systemName: "xmark") .foregroundColor(.secondary) .padding(8) .background( Circle().fill(Color(UIColor.tertiarySystemBackground)) ) } .buttonStyle(.plain) .accessibilityLabel("Dismiss") } } .padding(14) .background( RoundedRectangle(cornerRadius: 20, style: .continuous) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 20, style: .continuous) .strokeBorder(Color.white.opacity(0.15), lineWidth: 1) ) ) .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4) .accessibilityElement(children: .combine) .accessibilityLabel("Important notice for iOS 26 and later. Limited compatibility; improvements are ongoing.") } } private struct StatusDot: View { var color: Color @Environment(\.colorScheme) private var colorScheme var body: some View { ZStack { Circle().fill(color.opacity(0.25)).frame(width: 20, height: 20) Circle().fill(color).frame(width: 12, height: 12) .shadow(color: color.opacity(0.6), radius: 4, x: 0, y: 0) } .overlay( Circle().stroke(colorScheme == .dark ? Color.white.opacity(0.15) : Color.black.opacity(0.1), lineWidth: 0.5) ) } } private struct StatusGlyph: View { let icon: String let tint: Color var size: CGFloat = 48 var iconSize: CGFloat = 22 var body: some View { ZStack { Circle() .fill(tint.opacity(0.18)) .frame(width: size, height: size) Image(systemName: icon) .font(.system(size: iconSize, weight: .semibold, design: .rounded)) .foregroundStyle(tint) } .overlay( Circle() .stroke(Color.white.opacity(0.12), lineWidth: 0.5) ) } } private struct ChecklistRow: View { let item: ChecklistItem let accentColor: Color var body: some View { HStack(alignment: .center, spacing: 12) { StatusGlyph(icon: item.status.iconName, tint: item.status.tint, size: 40, iconSize: 18) VStack(alignment: .leading, spacing: 4) { Text(item.title) .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) Text(item.subtitle) .font(.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } Spacer(minLength: 0) if let actionTitle = item.actionTitle, let action = item.action { Button(action: action) { Text(actionTitle) .font(.caption.weight(.semibold)) .padding(.horizontal, 12) .padding(.vertical, 6) .background( Capsule(style: .continuous) .fill(accentColor.opacity(0.18)) ) .overlay( Capsule(style: .continuous) .stroke(accentColor.opacity(0.4), lineWidth: 1) ) } .buttonStyle(.plain) .foregroundStyle(accentColor) } } .padding(.horizontal, 12) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(Color(UIColor.secondarySystemBackground).opacity(0.55)) ) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) .stroke(Color.white.opacity(0.08), lineWidth: 1) ) } } private struct QuickConnectRow: View { let item: QuickConnectItem let accentColor: Color let isEnabled: Bool let action: () -> Void var body: some View { Button(action: action) { HStack(spacing: 14) { QuickAppBadge(title: item.displayName, accentColor: accentColor) VStack(alignment: .leading, spacing: 2) { Text(item.displayName) .font(.system(size: 16, weight: .semibold, design: .rounded)) .foregroundStyle(.primary) .lineLimit(1) .minimumScaleFactor(0.8) Text(item.bundleID) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) .textSelection(.enabled) } Spacer(minLength: 0) Image(systemName: "bolt.horizontal.circle.fill") .font(.system(size: 20, weight: .semibold)) .foregroundStyle(isEnabled ? accentColor : Color.secondary) } .padding(.vertical, 12) .padding(.horizontal, 14) .background( RoundedRectangle(cornerRadius: 18, style: .continuous) .fill(Color(UIColor.secondarySystemBackground).opacity(isEnabled ? 0.65 : 0.35)) ) .overlay( RoundedRectangle(cornerRadius: 18, style: .continuous) .stroke(Color.white.opacity(0.1), lineWidth: 1) ) } .buttonStyle(.plain) .disabled(!isEnabled) .opacity(isEnabled ? 1 : 0.55) } } private struct SystemPinnedRow: View { let item: SystemPinnedItem let accentColor: Color let isLaunching: Bool var action: () -> Void var onRemove: () -> Void var body: some View { Button(action: action) { HStack(spacing: 14) { QuickAppBadge(title: item.displayName, accentColor: accentColor) VStack(alignment: .leading, spacing: 2) { Text(item.displayName) .font(.system(size: 16, weight: .semibold, design: .rounded)) .foregroundStyle(.primary) .lineLimit(1) .minimumScaleFactor(0.8) Text(item.bundleID) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) .textSelection(.enabled) } Spacer(minLength: 0) if isLaunching { ProgressView() .controlSize(.small) .tint(accentColor) } else { Image(systemName: "play.fill") .font(.system(size: 18, weight: .semibold)) .foregroundStyle(accentColor) } } .padding(.vertical, 12) .padding(.horizontal, 14) .background( RoundedRectangle(cornerRadius: 18, style: .continuous) .fill(Color(UIColor.secondarySystemBackground).opacity(0.6)) ) .overlay( RoundedRectangle(cornerRadius: 18, style: .continuous) .stroke(Color.white.opacity(0.1), lineWidth: 1) ) } .buttonStyle(.plain) .disabled(isLaunching) .contextMenu { Button("Remove from Home".localized, systemImage: "star.slash") { onRemove() } } .swipeActions(edge: .trailing, allowsFullSwipe: false) { Button(role: .destructive) { onRemove() } label: { Label("Remove".localized, systemImage: "trash") } } } } private struct QuickAppBadge: View { let title: String let accentColor: Color private var initials: String { let words = title.split(separator: " ") if let first = words.first, !first.isEmpty { return String(first.prefix(1)).uppercased() } return String(title.prefix(1)).uppercased() } var body: some View { Text(initials) .font(.system(size: 16, weight: .semibold, design: .rounded)) .frame(width: 36, height: 36) .background( RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(accentColor.opacity(0.16)) ) .foregroundStyle(accentColor) } } private struct ReadinessSummary { let title: String let subtitle: String let icon: String let tint: Color } private struct PairingFileSignature: Equatable { let modificationDate: Date? let fileSize: UInt64 } private enum DeveloperModeStatus { case unknown case checking case enabled case disabled case error } private enum ChecklistStatus { case ready case waiting case attention case actionRequired var iconName: String { switch self { case .ready: return "checkmark.circle.fill" case .waiting: return "clock.fill" case .attention: return "exclamationmark.triangle.fill" case .actionRequired: return "exclamationmark.octagon.fill" } } var tint: Color { switch self { case .ready: return .green case .waiting: return .orange case .attention: return .yellow case .actionRequired: return .red } } } private struct ChecklistItem: Identifiable { let id = UUID() let title: String let subtitle: String let status: ChecklistStatus let actionTitle: String? let action: (() -> Void)? } private struct QuickConnectItem: Identifiable { let bundleID: String let displayName: String var id: String { bundleID } } private struct SystemPinnedItem: Identifiable { let bundleID: String let displayName: String var id: String { bundleID } } // MARK: - Connect-by-PID Sheet (minus/plus removed) private struct ConnectByPIDSheet: View { @Environment(\.dismiss) private var dismiss @Binding var recentPIDs: [Int] @State private var pidText: String = "" @State private var errorText: String? = nil @FocusState private var focused: Bool var onPasteCopyToast: () -> Void var onConnect: (Int) -> Void private var isValid: Bool { if let v = Int(pidText), v > 0 { return true } return false } private let capsuleHeight: CGFloat = 40 var body: some View { NavigationView { ZStack { Color.clear.ignoresSafeArea() ScrollView { VStack(spacing: 20) { VStack(alignment: .leading, spacing: 14) { Text("Enter a Process ID").font(.headline).foregroundColor(.primary) TextField("e.g. 1234", text: $pidText) .keyboardType(.numberPad) .textContentType(.oneTimeCode) .font(.system(.title3, design: .rounded)) .padding(12) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(.ultraThinMaterial) ) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .strokeBorder(Color.white.opacity(0.12), lineWidth: 1) ) .focused($focused) .onChange(of: pidText) { _, newVal in validate(newVal) } // Paste + Clear row HStack(spacing: 10) { CapsuleButton(systemName: "doc.on.clipboard", title: "Paste", height: capsuleHeight) { if let n = UIPasteboard.general.string?.trimmingCharacters(in: .whitespacesAndNewlines), let v = Int(n), v > 0 { pidText = String(v) validate(pidText) onPasteCopyToast() } else { errorText = "No valid PID on the clipboard." UIImpactFeedbackGenerator(style: .light).impactOccurred() } } CapsuleButton(systemName: "xmark", title: "Clear", height: capsuleHeight) { pidText = "" errorText = nil } } if let errorText { HStack(spacing: 6) { Image(systemName: "exclamationmark.triangle.fill").font(.footnote) Text(errorText).font(.footnote) } .foregroundColor(.orange) .transition(.opacity) } if !recentPIDs.isEmpty { VStack(alignment: .leading, spacing: 8) { Text("Recents") .font(.subheadline.weight(.semibold)) .foregroundColor(.secondary) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(recentPIDs, id: \.self) { pid in Button { pidText = String(pid); validate(pidText) } label: { Text("#\(pid)") .font(.footnote.weight(.semibold)) .padding(.vertical, 6) .padding(.horizontal, 10) .background( Capsule(style: .continuous) .fill(Color(UIColor.tertiarySystemBackground)) ) } .contextMenu { Button(role: .destructive) { removeRecent(pid) } label: { Label("Remove", systemImage: "trash") } } } } } } } Button { guard let pid = Int(pidText), pid > 0 else { return } onConnect(pid) addRecent(pid) dismiss() } label: { HStack { Image(systemName: "bolt.horizontal.circle").font(.system(size: 20)) Text("Connect") .font(.system(.title3, design: .rounded)) .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding(.vertical, 14) .background(Color.accentColor, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) .foregroundColor(Color.accentColor.contrastText()) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.2), lineWidth: 0.5) ) } .disabled(!isValid) .padding(.top, 8) } .padding(20) .background( RoundedRectangle(cornerRadius: 20, style: .continuous) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 20, style: .continuous) .strokeBorder(Color.white.opacity(0.15), lineWidth: 1) ) ) .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4) } .padding(.horizontal, 20) .padding(.vertical, 30) } } .navigationTitle("Connect by PID") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Cancel") { dismiss() } } } .onAppear { focused = true } } } // Small glassy square icon button private func iconSquareButton(systemName: String, action: @escaping () -> Void) -> some View { Button(action: action) { Image(systemName: systemName) .font(.headline) .frame(width: 36, height: 36) .background( RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(Color(UIColor.tertiarySystemBackground)) ) } .buttonStyle(.plain) .contentShape(Rectangle()) } private func validate(_ text: String) { if text.isEmpty { errorText = nil; return } if Int(text) == nil || Int(text)! <= 0 { errorText = "Please enter a positive number." } else { errorText = nil } } private func addRecent(_ pid: Int) { var list = recentPIDs.filter { $0 != pid } list.insert(pid, at: 0) if list.count > 8 { list = Array(list.prefix(8)) } recentPIDs = list } private func removeRecent(_ pid: Int) { recentPIDs.removeAll { $0 == pid } } private func prefillFromClipboardIfPossible() { if let s = UIPasteboard.general.string?.trimmingCharacters(in: .whitespacesAndNewlines), let v = Int(s), v > 0 { pidText = String(v); errorText = nil } } @ViewBuilder private func CapsuleButton(systemName: String, title: String, height: CGFloat = 40, action: @escaping () -> Void) -> some View { Button(action: action) { HStack(spacing: 6) { Image(systemName: systemName) Text(title).font(.subheadline.weight(.semibold)) } .frame(height: height) // enforce uniform height .padding(.horizontal, 12) .background(Capsule(style: .continuous).fill(Color(UIColor.tertiarySystemBackground))) } .buttonStyle(.plain) .contentShape(Rectangle()) } } #Preview { HomeView() } // MARK: - TXM detection public extension ProcessInfo { var hasTXM: Bool { if isTXMOverridden { return true } return { if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36), let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) { return access("\(file)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 } else { return (FileManager.default.filePath(atPath: "/private/preboot", withLength: 96).map { access("\($0)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 }) ?? false } }() } var isTXMOverridden: Bool { UserDefaults.standard.bool(forKey: UserDefaults.Keys.txmOverride) } }