From 1507c6490fb8e7713f7ba36bfac637f98ed8e95b Mon Sep 17 00:00:00 2001 From: superdooper86 Date: Thu, 7 May 2026 14:04:19 +0200 Subject: [PATCH] =?UTF-8?q?v2.0.4=20=E2=80=94=20Show=20spent,=20limit=20an?= =?UTF-8?q?d=20balance=20from=20dedicated=20billing=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ClaudeChecker/ContentView.swift | 23 ++++++++++++---- ClaudeChecker/Info.plist | 4 +-- ClaudeChecker/Models.swift | 44 ++++++++++++++++++++++-------- ClaudeChecker/UsageViewModel.swift | 37 ++++++++++++++++++++++++- README.md | 2 +- version.json | 6 ++-- 6 files changed, 92 insertions(+), 24 deletions(-) diff --git a/ClaudeChecker/ContentView.swift b/ClaudeChecker/ContentView.swift index f52dc82..2c7f764 100644 --- a/ClaudeChecker/ContentView.swift +++ b/ClaudeChecker/ContentView.swift @@ -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) } diff --git a/ClaudeChecker/Info.plist b/ClaudeChecker/Info.plist index 432a7c3..b5a9570 100644 --- a/ClaudeChecker/Info.plist +++ b/ClaudeChecker/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.0.3 + 2.0.4 CFBundleVersion - 10 + 11 LSMinimumSystemVersion 13.0 LSUIElement diff --git a/ClaudeChecker/Models.swift b/ClaudeChecker/Models.swift index cfd381f..cd016f1 100644 --- a/ClaudeChecker/Models.swift +++ b/ClaudeChecker/Models.swift @@ -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) } } diff --git a/ClaudeChecker/UsageViewModel.swift b/ClaudeChecker/UsageViewModel.swift index 92aa1fe..60c17f3 100644 --- a/ClaudeChecker/UsageViewModel.swift +++ b/ClaudeChecker/UsageViewModel.swift @@ -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] { diff --git a/README.md b/README.md index ffc9434..0c8e25e 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/version.json b/version.json index 47a6075..54aec02 100644 --- a/version.json +++ b/version.json @@ -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." }