// // Copyright 2026 Novaloop AG // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import Foundation import Observation /// Loads the own account's status for the banners that advertise it. /// Nothing is fetched until ``load()`` is called, so the settings screen decides when. @Observable final class HealthChatAccountStatusLoader { private(set) var status: HealthChatAccountStatus = .unknown @ObservationIgnored private let apiService: HealthChatAPIServiceProtocol? /// The load in flight — await this in tests before asserting. @ObservationIgnored @CancellableTask private(set) var loadingTask: Task? init(apiService: HealthChatAPIServiceProtocol) { self.apiService = apiService } /// Creates a loader with a fixed status, for previews. init(status: HealthChatAccountStatus) { self.status = status apiService = nil } /// Fetches the status again, replacing a load still in flight. func load() { guard let apiService else { return } loadingTask = Task { await load(from: apiService) } } // MARK: - Private private func load(from apiService: HealthChatAPIServiceProtocol) async { // Every failure below leaves the status untouched: a banner must not report an error. guard case let .success(profile) = await apiService.ownProfile(), !Task.isCancelled else { return } guard profile.isApproved else { status = .pendingApproval return } guard case let .success(completeness) = await apiService.profileCompleteness(), !Task.isCancelled else { return } // The server words the hint itself; without one there is nothing to show. guard !completeness.isComplete, let message = completeness.message else { status = .ok return } status = .incompleteProfile(message: message) } }