Files
oai-swift/oAI/Views/Screens/CreditsView.swift

179 lines
5.7 KiB
Swift

//
// 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 <https://www.gnu.org/licenses/>.
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)
}