v2.0.4 — Show spent, limit and balance from dedicated billing endpoints
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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] {
|
||||
|
||||
Reference in New Issue
Block a user