import SwiftUI @preconcurrency import NetworkExtension import LocalAuthentication import ProxyCore import GRDB @Observable @MainActor final class AppState { var vpnStatus: NEVPNStatus = .disconnected var isCertificateInstalled: Bool = false var isCertificateTrusted: Bool = false var isLocked: Bool = false var runtimeStatus = ProxyRuntimeStatus() var isAppLockEnabled: Bool { get { UserDefaults.standard.bool(forKey: "appLockEnabled") } set { UserDefaults.standard.set(newValue, forKey: "appLockEnabled") } } private var vpnManager: NETunnelProviderManager? private var statusObservation: NSObjectProtocol? @ObservationIgnored private var runtimeObservation: AnyDatabaseCancellable? private let runtimeStatusRepo = RuntimeStatusRepository() init() { if UserDefaults.standard.bool(forKey: "appLockEnabled") { isLocked = true } isCertificateInstalled = CertificateManager.shared.hasCA Task { await loadVPNManager() } observeRuntimeStatus() } func authenticate() { let context = LAContext() var error: NSError? guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { isLocked = false return } context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Unlock Proxy") { [weak self] success, _ in Task { @MainActor in if success { self?.isLocked = false } } } } func loadVPNManager() async { do { let managers = try await NETunnelProviderManager.loadAllFromPreferences() if let existing = managers.first { vpnManager = existing } else { let manager = NETunnelProviderManager() let proto = NETunnelProviderProtocol() proto.providerBundleIdentifier = ProxyConstants.extensionBundleIdentifier proto.serverAddress = ProxyConstants.proxyHost manager.protocolConfiguration = proto manager.localizedDescription = "Proxy" manager.isEnabled = true try await manager.saveToPreferences() try await manager.loadFromPreferences() vpnManager = manager } observeVPNStatus() vpnStatus = vpnManager?.connection.status ?? .disconnected } catch { print("[AppState] Failed to load VPN manager: \(error)") } } func toggleVPN() async { guard let manager = vpnManager else { await loadVPNManager() return } switch manager.connection.status { case .connected, .connecting: manager.connection.stopVPNTunnel() case .disconnected, .invalid: do { // Ensure saved and fresh before starting manager.isEnabled = true try await manager.saveToPreferences() try await manager.loadFromPreferences() try manager.connection.startVPNTunnel() } catch { print("[AppState] Failed to start VPN: \(error)") } default: break } } var isVPNConnected: Bool { vpnStatus == .connected } var hasSharedCertificate: Bool { guard let localFingerprint = CertificateManager.shared.caFingerprint else { return false } return localFingerprint == runtimeStatus.caFingerprint } var isHTTPSInspectionVerified: Bool { runtimeStatus.lastSuccessfulMITMAt != nil } var lastRuntimeError: String? { runtimeStatus.lastMITMError ?? runtimeStatus.lastConnectError ?? runtimeStatus.lastProxyError } var vpnStatusText: String { switch vpnStatus { case .connected: "Connected" case .connecting: "Connecting..." case .disconnecting: "Disconnecting..." case .disconnected: "Disconnected" case .invalid: "Not Configured" case .reasserting: "Reconnecting..." @unknown default: "Unknown" } } private func observeVPNStatus() { guard let manager = vpnManager else { return } // Remove existing observer if let existing = statusObservation { NotificationCenter.default.removeObserver(existing) } statusObservation = NotificationCenter.default.addObserver( forName: .NEVPNStatusDidChange, object: manager.connection, queue: .main ) { [weak self] _ in Task { @MainActor in self?.vpnStatus = manager.connection.status } } } private func observeRuntimeStatus() { runtimeObservation = runtimeStatusRepo.observeStatus() .start(in: DatabaseManager.shared.dbPool) { error in ProxyLogger.ui.error("AppState runtime observation error: \(error.localizedDescription)") } onChange: { [weak self] status in Task { @MainActor in self?.runtimeStatus = status self?.isCertificateInstalled = CertificateManager.shared.hasCA self?.isCertificateTrusted = status.lastSuccessfulMITMAt != nil } } } }