import Client import ComposableArchitecture import Foundation import LaunchAgentManager import Status import SwiftUI import XPCShared import Logger @Reducer public struct General { @ObservableState public struct State: Equatable { var xpcServiceVersion: String? var xpcCLSVersion: String? var isAccessibilityPermissionGranted: ObservedAXStatus = .unknown var isExtensionPermissionGranted: ExtensionPermissionStatus = .unknown var xpcServiceAuthStatus: AuthStatus = .init(status: .unknown) var isReloading = false } public enum Action: Equatable { case appear case setupLaunchAgentIfNeeded case openExtensionManager case reloadStatus case finishReloading( xpcServiceVersion: String, xpcCLSVersion: String?, axStatus: ObservedAXStatus, extensionStatus: ExtensionPermissionStatus, authStatus: AuthStatus ) case failedReloading case retryReloading } @Dependency(\.toast) var toast struct ReloadStatusCancellableId: Hashable {} public var body: some ReducerOf { Reduce { state, action in switch action { case .appear: return .run { send in await send(.setupLaunchAgentIfNeeded) for await _ in DistributedNotificationCenter.default().notifications(named: .serviceStatusDidChange) { await send(.reloadStatus) } } case .setupLaunchAgentIfNeeded: return .run { send in #if DEBUG // do not auto install on debug build await send(.reloadStatus) #else Task { do { try await LaunchAgentManager() .setupLaunchAgentForTheFirstTimeIfNeeded() } catch { Logger.ui.error("Failed to setup launch agent. \(error.localizedDescription)") toast("Operation failed: permission denied. This may be due to missing background permissions.", .error) } await send(.reloadStatus) } #endif } case .openExtensionManager: return .run { send in let service = try getService() do { _ = try await service .send(requestBody: ExtensionServiceRequests.OpenExtensionManager()) } catch { Logger.ui.error("Failed to open extension manager. \(error.localizedDescription)") toast(error.localizedDescription, .error) await send(.failedReloading) } } case .reloadStatus: guard !state.isReloading else { return .none } state.isReloading = true return .run { send in let service = try getService() do { let isCommunicationReady = try await service.launchIfNeeded() if isCommunicationReady { let xpcServiceVersion = try await service.getXPCServiceVersion().version let isAccessibilityPermissionGranted = try await service .getXPCServiceAccessibilityPermission() let isExtensionPermissionGranted = try await service.getXPCServiceExtensionPermission() let xpcServiceAuthStatus = try await service.getXPCServiceAuthStatus() ?? .init(status: .unknown) let xpcCLSVersion = try await service.getXPCCLSVersion() await send(.finishReloading( xpcServiceVersion: xpcServiceVersion, xpcCLSVersion: xpcCLSVersion, axStatus: isAccessibilityPermissionGranted, extensionStatus: isExtensionPermissionGranted, authStatus: xpcServiceAuthStatus )) } else { toast("Launching service app.", .info) try await Task.sleep(nanoseconds: 5_000_000_000) await send(.retryReloading) } } catch let error as XPCCommunicationBridgeError { Logger.ui.error("Failed to reach communication bridge. \(error.localizedDescription)") toast( "Unable to connect to the communication bridge. The helper application didn't respond. This may be due to missing background permissions.", .error ) await send(.failedReloading) } catch { Logger.ui.error("Failed to reload status. \(error.localizedDescription)") toast(error.localizedDescription, .error) await send(.failedReloading) } }.cancellable(id: ReloadStatusCancellableId(), cancelInFlight: true) case let .finishReloading(version, clsVersion, axStatus, extensionStatus, authStatus): state.xpcServiceVersion = version state.isAccessibilityPermissionGranted = axStatus state.isExtensionPermissionGranted = extensionStatus state.xpcServiceAuthStatus = authStatus state.xpcCLSVersion = clsVersion state.isReloading = false return .none case .failedReloading: state.isReloading = false return .none case .retryReloading: state.isReloading = false return .run { send in await send(.reloadStatus) } } } } }