Switches the project from AGPL to a source-available license that restricts commercial use — selling oAI or any part of it, standalone or bundled into another product/service, now requires a separate commercial license from the copyright holder. Noncommercial use, study, modification, and sharing remain fully permitted. Updates: LICENSE (canonical PolyForm Noncommercial 1.0.0 text + commercial licensing contact note), SPDX headers and file-header boilerplate across all Swift source files, the in-app About dialog's license link (+ its localization catalog entry), README.md and DEVELOPMENT.md license sections. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
177 lines
5.6 KiB
Swift
177 lines
5.6 KiB
Swift
//
|
|
// CreditsView.swift
|
|
// oAI
|
|
//
|
|
// Account credits and balance
|
|
//
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
// Copyright (C) 2026 Rune Olsen
|
|
//
|
|
// This file is part of oAI.
|
|
//
|
|
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
|
|
// You may use, study, modify, and share it for any noncommercial
|
|
// purpose. Commercial use — including selling oAI or any part of
|
|
// it, standalone or bundled into another product or service —
|
|
// requires a separate commercial license from the copyright holder.
|
|
//
|
|
// See the LICENSE file or
|
|
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
|
|
// the full license text. For commercial licensing, contact Rune
|
|
// Olsen via <https://mac.oai.pm>.
|
|
|
|
|
|
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: LocalizedStringKey
|
|
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)
|
|
}
|