v2.0.4 — Show spent, limit and balance from dedicated billing endpoints

This commit is contained in:
superdooper86
2026-05-07 14:04:19 +02:00
parent ccf3468621
commit 1507c6490f
6 changed files with 92 additions and 24 deletions
+17 -6
View File
@@ -57,7 +57,7 @@ struct ContentView: View {
// Extra credits row
if let extra = vm.extraUsage, extra.isEnabled {
ExtraUsageRow(extra: extra)
ExtraUsageRow(extra: extra, prepaid: vm.prepaidCredits, overage: vm.overageSpendLimit)
.padding(.horizontal, 16)
.padding(.top, 12)
}
@@ -490,10 +490,20 @@ struct FooterBar: View {
struct ExtraUsageRow: View {
let extra: ExtraUsage
let prepaid: PrepaidCredits?
let overage: OverageSpendLimit?
var pct: Double { min(100, max(0, extra.utilization ?? 0)) }
var cur: String { overage?.currency ?? extra.currency ?? "" }
var spentCents: Double? { overage?.usedCredits }
var limitCents: Double? { overage?.monthlyCreditLimit }
var balanceCents: Double? { prepaid?.amount }
var pct: Double {
guard let s = spentCents, let l = limitCents, l > 0 else { return 0 }
return min(100, max(0, (s / l) * 100))
}
var color: Color { pct > 80 ? .red : pct > 50 ? .orange : .green }
var cur: String { extra.currency ?? "" }
func fmt(_ cents: Double?) -> String {
guard let cents else { return "" }
@@ -511,10 +521,11 @@ struct ExtraUsageRow: View {
.foregroundColor(.secondary)
}
VStack(spacing: 4) {
ExtraUsageLine(label: "Spent", value: "\(cur) \(fmt(extra.usedCredits))", color: color)
ExtraUsageLine(label: "Limit", value: extra.monthlyLimit != nil ? "\(cur) \(fmt(extra.monthlyLimit))" : "Unlimited", color: .secondary)
ExtraUsageLine(label: "Spent", value: "\(cur) \(fmt(spentCents))", color: color)
ExtraUsageLine(label: "Limit", value: limitCents != nil ? "\(cur) \(fmt(limitCents))" : "Unlimited", color: .secondary)
ExtraUsageLine(label: "Balance", value: "\(cur) \(fmt(balanceCents))", color: .primary)
}
if extra.monthlyLimit != nil {
if limitCents != nil {
ProgressBar(value: pct / 100, color: color)
.frame(height: 4)
}
+2 -2
View File
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.0.3</string>
<string>2.0.4</string>
<key>CFBundleVersion</key>
<string>10</string>
<string>11</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
+33 -11
View File
@@ -83,24 +83,46 @@ struct UsageWindow: Codable {
struct ExtraUsage: Codable {
let isEnabled: Bool
let monthlyLimit: Double?
let usedCredits: Double?
let utilization: Double?
let currency: String?
enum CodingKeys: String, CodingKey {
case isEnabled = "is_enabled"
case monthlyLimit = "monthly_limit"
case usedCredits = "used_credits"
case utilization
case currency
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
isEnabled = (try? c.decodeIfPresent(Bool.self, forKey: .isEnabled)) ?? false
monthlyLimit = try? c.decodeIfPresent(Double.self, forKey: .monthlyLimit)
usedCredits = try? c.decodeIfPresent(Double.self, forKey: .usedCredits)
utilization = try? c.decodeIfPresent(Double.self, forKey: .utilization)
currency = try? c.decodeIfPresent(String.self, forKey: .currency)
isEnabled = (try? c.decodeIfPresent(Bool.self, forKey: .isEnabled)) ?? false
currency = try? c.decodeIfPresent(String.self, forKey: .currency)
}
}
struct PrepaidCredits: Decodable {
let amount: Double?
let currency: String?
enum CodingKeys: String, CodingKey { case amount, currency }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
amount = try? c.decodeIfPresent(Double.self, forKey: .amount)
currency = try? c.decodeIfPresent(String.self, forKey: .currency)
}
}
struct OverageSpendLimit: Decodable {
let isEnabled: Bool
let usedCredits: Double?
let monthlyCreditLimit: Double?
let currency: String?
enum CodingKeys: String, CodingKey {
case isEnabled = "is_enabled"
case usedCredits = "used_credits"
case monthlyCreditLimit = "monthly_credit_limit"
case currency
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
isEnabled = (try? c.decodeIfPresent(Bool.self, forKey: .isEnabled)) ?? false
usedCredits = try? c.decodeIfPresent(Double.self, forKey: .usedCredits)
monthlyCreditLimit = try? c.decodeIfPresent(Double.self, forKey: .monthlyCreditLimit)
currency = try? c.decodeIfPresent(String.self, forKey: .currency)
}
}
+36 -1
View File
@@ -9,6 +9,8 @@ class UsageViewModel: ObservableObject {
@Published var lastUpdated: Date?
@Published var errorMessage: String?
@Published var extraUsage: ExtraUsage?
@Published var prepaidCredits: PrepaidCredits?
@Published var overageSpendLimit: OverageSpendLimit?
@Published var planLabel: String = "Claude"
@Published var isNotAuthenticated: Bool = false
@Published var triggerLogin: Bool = false
@@ -42,9 +44,14 @@ class UsageViewModel: ObservableObject {
do {
let orgId = cachedOrgId!
let usage = try await fetchUsage(orgId: orgId)
async let usageFetch = fetchUsage(orgId: orgId)
async let prepaidFetch = fetchPrepaidCredits(orgId: orgId)
async let overageFetch = fetchOverageSpendLimit(orgId: orgId)
let (usage, prepaid, overage) = try await (usageFetch, prepaidFetch, overageFetch)
limits = buildLimits(from: usage)
extraUsage = usage.extraUsage
prepaidCredits = prepaid
overageSpendLimit = overage
planLabel = "Pro"
lastUpdated = Date()
@@ -97,6 +104,34 @@ class UsageViewModel: ObservableObject {
return try JSONDecoder().decode(UsageResponse.self, from: data)
}
private func fetchPrepaidCredits(orgId: String) async throws -> PrepaidCredits? {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/prepaid/credits")!
var req = URLRequest(url: url)
req.setValue("application/json", forHTTPHeaderField: "accept")
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
let claudeCookies = cookies.filter { $0.domain.contains("claude.ai") }
if let header = HTTPCookie.requestHeaderFields(with: claudeCookies)["Cookie"] {
req.setValue(header, forHTTPHeaderField: "Cookie")
}
let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
return try? JSONDecoder().decode(PrepaidCredits.self, from: data)
}
private func fetchOverageSpendLimit(orgId: String) async throws -> OverageSpendLimit? {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/overage_spend_limit")!
var req = URLRequest(url: url)
req.setValue("application/json", forHTTPHeaderField: "accept")
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
let claudeCookies = cookies.filter { $0.domain.contains("claude.ai") }
if let header = HTTPCookie.requestHeaderFields(with: claudeCookies)["Cookie"] {
req.setValue(header, forHTTPHeaderField: "Cookie")
}
let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
return try? JSONDecoder().decode(OverageSpendLimit.self, from: data)
}
// MARK: - Build limits
private func buildLimits(from usage: UsageResponse) -> [AgentLimit] {
+1 -1
View File
@@ -8,7 +8,7 @@
[![macOS](https://img.shields.io/badge/macOS-13.0+-000000?style=flat&logo=apple&logoColor=white)](https://www.apple.com/macos/)
[![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org)
[![Version](https://img.shields.io/badge/version-2.0.3-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases)
[![Version](https://img.shields.io/badge/version-2.0.4-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases)
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
</div>
+3 -3
View File
@@ -1,5 +1,5 @@
{
"version": "2.0.3",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v2.0.3/ClaudeChecker.zip",
"notes": "Fixed extra usage section."
"version": "2.0.4",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v2.0.4/ClaudeChecker.zip",
"notes": "Extra usage now shows spent, limit, and current balance."
}