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
|
// Extra credits row
|
||||||
if let extra = vm.extraUsage, extra.isEnabled {
|
if let extra = vm.extraUsage, extra.isEnabled {
|
||||||
ExtraUsageRow(extra: extra)
|
ExtraUsageRow(extra: extra, prepaid: vm.prepaidCredits, overage: vm.overageSpendLimit)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
}
|
}
|
||||||
@@ -490,10 +490,20 @@ struct FooterBar: View {
|
|||||||
|
|
||||||
struct ExtraUsageRow: View {
|
struct ExtraUsageRow: View {
|
||||||
let extra: ExtraUsage
|
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 color: Color { pct > 80 ? .red : pct > 50 ? .orange : .green }
|
||||||
var cur: String { extra.currency ?? "" }
|
|
||||||
|
|
||||||
func fmt(_ cents: Double?) -> String {
|
func fmt(_ cents: Double?) -> String {
|
||||||
guard let cents else { return "—" }
|
guard let cents else { return "—" }
|
||||||
@@ -511,10 +521,11 @@ struct ExtraUsageRow: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
ExtraUsageLine(label: "Spent", value: "\(cur) \(fmt(extra.usedCredits))", color: color)
|
ExtraUsageLine(label: "Spent", value: "\(cur) \(fmt(spentCents))", color: color)
|
||||||
ExtraUsageLine(label: "Limit", value: extra.monthlyLimit != nil ? "\(cur) \(fmt(extra.monthlyLimit))" : "Unlimited", color: .secondary)
|
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)
|
ProgressBar(value: pct / 100, color: color)
|
||||||
.frame(height: 4)
|
.frame(height: 4)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.0.3</string>
|
<string>2.0.4</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>10</string>
|
<string>11</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>13.0</string>
|
<string>13.0</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
|
|||||||
+33
-11
@@ -83,24 +83,46 @@ struct UsageWindow: Codable {
|
|||||||
|
|
||||||
struct ExtraUsage: Codable {
|
struct ExtraUsage: Codable {
|
||||||
let isEnabled: Bool
|
let isEnabled: Bool
|
||||||
let monthlyLimit: Double?
|
|
||||||
let usedCredits: Double?
|
|
||||||
let utilization: Double?
|
|
||||||
let currency: String?
|
let currency: String?
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case isEnabled = "is_enabled"
|
case isEnabled = "is_enabled"
|
||||||
case monthlyLimit = "monthly_limit"
|
|
||||||
case usedCredits = "used_credits"
|
|
||||||
case utilization
|
|
||||||
case currency
|
case currency
|
||||||
}
|
}
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
isEnabled = (try? c.decodeIfPresent(Bool.self, forKey: .isEnabled)) ?? false
|
isEnabled = (try? c.decodeIfPresent(Bool.self, forKey: .isEnabled)) ?? false
|
||||||
monthlyLimit = try? c.decodeIfPresent(Double.self, forKey: .monthlyLimit)
|
currency = try? c.decodeIfPresent(String.self, forKey: .currency)
|
||||||
usedCredits = try? c.decodeIfPresent(Double.self, forKey: .usedCredits)
|
}
|
||||||
utilization = try? c.decodeIfPresent(Double.self, forKey: .utilization)
|
}
|
||||||
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 lastUpdated: Date?
|
||||||
@Published var errorMessage: String?
|
@Published var errorMessage: String?
|
||||||
@Published var extraUsage: ExtraUsage?
|
@Published var extraUsage: ExtraUsage?
|
||||||
|
@Published var prepaidCredits: PrepaidCredits?
|
||||||
|
@Published var overageSpendLimit: OverageSpendLimit?
|
||||||
@Published var planLabel: String = "Claude"
|
@Published var planLabel: String = "Claude"
|
||||||
@Published var isNotAuthenticated: Bool = false
|
@Published var isNotAuthenticated: Bool = false
|
||||||
@Published var triggerLogin: Bool = false
|
@Published var triggerLogin: Bool = false
|
||||||
@@ -42,9 +44,14 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let orgId = cachedOrgId!
|
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)
|
limits = buildLimits(from: usage)
|
||||||
extraUsage = usage.extraUsage
|
extraUsage = usage.extraUsage
|
||||||
|
prepaidCredits = prepaid
|
||||||
|
overageSpendLimit = overage
|
||||||
planLabel = "Pro"
|
planLabel = "Pro"
|
||||||
lastUpdated = Date()
|
lastUpdated = Date()
|
||||||
|
|
||||||
@@ -97,6 +104,34 @@ class UsageViewModel: ObservableObject {
|
|||||||
return try JSONDecoder().decode(UsageResponse.self, from: data)
|
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
|
// MARK: - Build limits
|
||||||
|
|
||||||
private func buildLimits(from usage: UsageResponse) -> [AgentLimit] {
|
private func buildLimits(from usage: UsageResponse) -> [AgentLimit] {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
[](https://www.apple.com/macos/)
|
[](https://www.apple.com/macos/)
|
||||||
[](https://swift.org)
|
[](https://swift.org)
|
||||||
[](https://github.com/superdooper86/claudechecker/releases)
|
[](https://github.com/superdooper86/claudechecker/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.3",
|
"version": "2.0.4",
|
||||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v2.0.3/ClaudeChecker.zip",
|
"url": "https://github.com/superdooper86/claudechecker/releases/download/v2.0.4/ClaudeChecker.zip",
|
||||||
"notes": "Fixed extra usage section."
|
"notes": "Extra usage now shows spent, limit, and current balance."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user