Files
steipete--codexbar/Sources/CodexBar/CodexAccountPromotionCoordinator.swift
2026-07-13 12:22:33 +08:00

115 lines
4.6 KiB
Swift

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<CodexAccountPromotionResult, CodexSystemAccountPromotionUserFacingError>
{
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)
}
}