// // CreditsView.swift // oAI // // Account credits and balance // // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Rune Olsen // // This file is part of oAI. // // oAI is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // oAI is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General // Public License for more details. // // You should have received a copy of the GNU Affero General Public // License along with oAI. If not, see . import SwiftUI struct CreditsView: View { let provider: Settings.Provider @Environment(\.dismiss) var dismiss @State private var credits: Credits? @State private var isLoading = false @State private var errorMessage: String? var body: some View { NavigationStack { VStack(spacing: 24) { // Provider icon Image(systemName: provider.iconName) .font(.system(size: 60)) .foregroundColor(Color.providerColor(provider)) Text(provider.displayName) .font(.title2) .fontWeight(.semibold) Divider() // Credits info based on provider VStack(spacing: 16) { switch provider { case .openrouter: openRouterCreditsView case .anthropic: Text("Anthropic Balance") .font(.headline) Text("Check your balance at:") .font(.caption) .foregroundColor(.secondary) Link("console.anthropic.com", destination: URL(string: "https://console.anthropic.com")!) .font(.body) case .openai: Text("OpenAI Balance") .font(.headline) Text("Check your usage at:") .font(.caption) .foregroundColor(.secondary) Link("platform.openai.com", destination: URL(string: "https://platform.openai.com/usage")!) .font(.body) case .ollama: Text("Ollama (Local)") .font(.headline) Text("Running locally — no credits needed!") .font(.body) .foregroundColor(.secondary) Image(systemName: "checkmark.circle.fill") .font(.system(size: 40)) .foregroundColor(.green) .padding(.top) } } Spacer() } .padding() .navigationTitle("Credits") .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } } } } .task { await fetchCredits() } } // MARK: - OpenRouter Credits @ViewBuilder private var openRouterCreditsView: some View { Text("OpenRouter Credits") .font(.headline) if isLoading { ProgressView("Loading...") .padding() } else if let error = errorMessage { Text(error) .font(.caption) .foregroundColor(.red) .padding() Button("Retry") { Task { await fetchCredits() } } } else if let credits = credits { VStack(spacing: 12) { CreditRow(label: "Remaining", value: credits.balanceDisplay, highlight: true) Divider() if let limit = credits.limit { CreditRow(label: "Total Credits", value: String(format: "$%.2f", limit)) } if let usage = credits.usage { CreditRow(label: "Used", value: String(format: "$%.2f", usage)) } } } else { Text("No credit data available") .font(.caption) .foregroundColor(.secondary) } } private func fetchCredits() async { guard provider == .openrouter else { return } guard let apiProvider = ProviderRegistry.shared.getCurrentProvider() else { errorMessage = "No API key configured" return } isLoading = true errorMessage = nil do { credits = try await apiProvider.getCredits() isLoading = false } catch { errorMessage = error.localizedDescription isLoading = false } } } struct CreditRow: View { let label: String let value: String var highlight: Bool = false var body: some View { HStack { Text(label) .foregroundColor(highlight ? .primary : .secondary) .fontWeight(highlight ? .semibold : .regular) Spacer() Text(value) .font(highlight ? .title2.monospacedDigit() : .body.monospacedDigit()) .fontWeight(highlight ? .bold : .medium) .foregroundColor(highlight ? .green : .primary) } } } #Preview { CreditsView(provider: .openrouter) }