// // Account.swift // meterios // // (c) 2025 Martin "maride" Dessauer // import Foundation import OSLog enum AccountState: Int { case Unknown // No connection attempt has been made yet; basically the startup state case Disconnected // Not connected as per user request case Failure // Last connection failed case Connected // Last connection succeeded } /// **Account** resembles an user account in Mete and provides relevant functions to Meterios, like encoding and decoding, the set-up client for this very account and favourites management @Observable class Account: Identifiable, Codable { public let id: UUID = UUID() private(set) var state: AccountState = .Unknown private let client: MeteClient public let address: String public let userID: Int private(set) var displayName: String? private(set) var displayImageURL: URL? private(set) var balance: Float? private var favourites: [Int] enum CodingKeys: String, CodingKey { case address, userID, favourites } init(address: String, userID: Int, favourites: [Int] = []) { self.address = address self.userID = userID self.favourites = favourites self.client = MeteClient(address: address, userID: userID) } required convenience init(from decoder : Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let address = try container.decode(String.self, forKey: .address) let userID = try container.decode(Int.self, forKey: .userID) let favourites = try container.decode([Int].self, forKey: .favourites) self.init(address: address, userID: userID, favourites: favourites) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(address, forKey: .address) try container.encode(userID, forKey: .userID) try container.encode(favourites, forKey: .favourites) } // Ping updates the state of the account by trying to connect to the backend func Ping() async { do { state = try await client.CanConnect() ? .Connected : .Disconnected } catch { state = .Failure } } // Refresh updates the account metadata; failure to do so changes the account state func Refresh() async { do { let user = try await client.GetUser() displayName = user.displayName displayImageURL = user.displayImageURL balance = user.balance state = .Connected } catch { let _ = Logger().error("Unable to refresh user account: \(error)") state = .Failure } } // GetDrinks returns all drinks for this account and backend func GetDrinks() async throws -> Array { return try await client.GetDrinks() } // BookDrink books the specified drink using this account func BookDrink(_ drinkID: Int) async throws { try await client.BookDrink(drinkID) await Refresh() } // IsFavourite returns true if the given drink ID is a favourite func IsFavourite(_ drinkID: Int) -> Bool { return favourites.contains(drinkID) } // AddFavourite adds the given drink ID to the list of favourites func AddFavourite(_ drinkID: Int) { if !IsFavourite(drinkID) { favourites.append(drinkID) } } // RemoveFavourite removes the given drink ID from the favourites list func RemoveFavourite(_ drinkID: Int) { let index = favourites.firstIndex(where: { $0 == drinkID }) if index != nil { favourites.remove(at: index!) } } // SetFavourite sets if the given drink ID is a favourite func SetFavourite(drinkID: Int, shouldBeFavourite: Bool) { if IsFavourite(drinkID) && !shouldBeFavourite { RemoveFavourite(drinkID) } else if !IsFavourite(drinkID) && shouldBeFavourite { AddFavourite(drinkID) } } }