import Foundation import Observation struct CodexSystemAccountPromotionUserFacingError: Error, Equatable { let title: String let message: String } @MainActor @Observable final class CodexAccountPromotionCoordinator { let service: CodexAccountPromotionService weak var managedAccountCoordinator: ManagedCodexAccountCoordinator? private(set) var isAuthenticatingLiveAccount = false private(set) var isPromotingSystemAccount = false private(set) var userFacingError: CodexSystemAccountPromotionUserFacingError? init( service: CodexAccountPromotionService, managedAccountCoordinator: ManagedCodexAccountCoordinator? = nil) { self.service = service self.managedAccountCoordinator = managedAccountCoordinator } convenience init( settingsStore: SettingsStore, usageStore: UsageStore, managedAccountCoordinator: ManagedCodexAccountCoordinator? = nil) { self.init( service: CodexAccountPromotionService(settingsStore: settingsStore, usageStore: usageStore), managedAccountCoordinator: managedAccountCoordinator) } func promote(managedAccountID: UUID) async -> Result { self.userFacingError = nil guard !self.isInteractionBlocked() else { let error = Self.interactionBlockedError() self.userFacingError = error return .failure(error) } self.isPromotingSystemAccount = true defer { self.isPromotingSystemAccount = false } do { let result = try await self.service.promoteManagedAccount(id: managedAccountID) return .success(result) } catch { let mapped = Self.mapUserFacingError(error) self.userFacingError = mapped return .failure(mapped) } } func clearError() { self.userFacingError = nil } func setLiveReauthenticationInProgress(_ isInProgress: Bool) { self.isAuthenticatingLiveAccount = isInProgress } func isInteractionBlocked() -> Bool { self.isPromotingSystemAccount || self.isAuthenticatingLiveAccount || self.managedAccountCoordinator?.hasConflictingManagedAccountOperationInFlight == true } private static func interactionBlockedError() -> CodexSystemAccountPromotionUserFacingError { CodexSystemAccountPromotionUserFacingError( title: L("Could not switch system account"), message: L("Finish the current managed account change before switching the system account.")) } static func mapUserFacingError(_ error: Error) -> CodexSystemAccountPromotionUserFacingError { let title = L("Could not switch system account") if let error = error as? CodexAccountPromotionError { let message = switch error { case .targetManagedAccountNotFound: L("That account is no longer available in CodexBar. Refresh the account list and try again.") case .targetManagedAccountAuthMissing: L("CodexBar could not find saved auth for that account. Re-authenticate it and try again.") case .targetManagedAccountAuthUnreadable: L("CodexBar could not read saved auth for that account. Re-authenticate it and try again.") case .liveAccountUnreadable: L("CodexBar could not read the current system account on this Mac.") case .liveAccountMissingIdentityForPreservation: L("CodexBar could not safely preserve the current system account before switching.") case .liveAccountAPIKeyOnlyUnsupported: L("CodexBar can't replace a system account that is signed in with an API key only setup.") case .displacedLiveManagedAccountConflict: L( "CodexBar found another managed account that already uses the current system account. " + "Resolve the duplicate account before switching.") case .displacedLiveImportFailed: L("CodexBar could not save the current system account before switching.") case .managedStoreCommitFailed: L("CodexBar could not update managed account storage.") case .liveAuthSwapFailed: L("CodexBar could not replace the live Codex auth on this Mac.") } return CodexSystemAccountPromotionUserFacingError(title: title, message: message) } return CodexSystemAccountPromotionUserFacingError(title: title, message: error.localizedDescription) } }