Compare commits

..
Author SHA1 Message Date
SuperDooper 0aa8837b2f beta.16: anchor background WebView in hidden NSWindow to prevent WebKit throttling 2026-05-11 11:47:09 +02:00
SuperDooper 373ca1dc14 beta.16: anchor background WebView in hidden NSWindow to prevent WebKit throttling 2026-05-11 11:47:08 +02:00
SuperDooper 7a4c21768f beta.16: anchor background WebView in hidden NSWindow to prevent WebKit throttling 2026-05-11 11:47:06 +02:00
SuperDooper 7b26629dc7 beta.16: anchor background WebView in hidden NSWindow to prevent WebKit throttling 2026-05-11 11:47:05 +02:00
github-actions[bot] e446ea97f9 Beta release v1.2.1-beta.15 2026-05-11 09:43:20 +00:00
SuperDooper 36af325e2d beta.15: only detect auth when back on claude.ai, not on OAuth provider pages 2026-05-11 11:42:26 +02:00
SuperDooper e2ef8b6f2a beta.15: only detect auth when back on claude.ai, not on OAuth provider pages 2026-05-11 11:42:25 +02:00
SuperDooper 7a4975fa3e beta.15: only detect auth when back on claude.ai, not on OAuth provider pages 2026-05-11 11:42:24 +02:00
github-actions[bot] c9912f8463 Beta release v1.2.1-beta.14 2026-05-11 09:30:35 +00:00
SuperDooper 238614a94b beta.14: fix httpShouldHandleCookies=false so Cookie header is actually sent 2026-05-11 11:29:40 +02:00
SuperDooper b11507221f beta.14: fix httpShouldHandleCookies=false so Cookie header is actually sent 2026-05-11 11:29:38 +02:00
SuperDooper b6fef53264 beta.14: fix httpShouldHandleCookies=false so Cookie header is actually sent 2026-05-11 11:29:37 +02:00
SuperDooper ec6ae6621a beta.14: fix httpShouldHandleCookies=false so Cookie header is actually sent 2026-05-11 11:29:35 +02:00
github-actions[bot] 105a7706fa Beta release v1.2.1-beta.13 2026-05-11 09:21:33 +00:00
SuperDooper a950392a6f beta.13: wait for apiWebView didFinish before making JS fetch calls 2026-05-11 11:20:51 +02:00
SuperDooper 8087cc029d beta.13: wait for apiWebView didFinish before making JS fetch calls 2026-05-11 11:20:50 +02:00
SuperDooper 11f9e0c9b6 beta.13: wait for apiWebView didFinish before making JS fetch calls 2026-05-11 11:20:49 +02:00
github-actions[bot] cc835ee2b0 Beta release v1.2.1-beta.12 2026-05-11 09:03:50 +00:00
SuperDooper 8e7328b2c5 beta.12: route all API calls through background WKWebView 2026-05-11 11:02:34 +02:00
SuperDooper 709eb12382 beta.12: route all API calls through background WKWebView 2026-05-11 11:02:33 +02:00
SuperDooper e866324a48 beta.12: route all API calls through background WKWebView 2026-05-11 11:02:32 +02:00
SuperDooper a62584221b beta.12: route all API calls through background WKWebView 2026-05-11 11:02:30 +02:00
github-actions[bot] fc7fd19652 Beta release v1.2.1-beta.11 2026-05-11 08:47:11 +00:00
superdooper86 58b947e515 fix: add KVO on webView.url to catch SPA pushState navigation
didFinish only fires for cross-document (full page) navigations. After
loading https://claude.ai/login the SPA redirects authenticated users
via history.pushState to /new — this changes the URL visually but never
fires didFinish, so auth was never detected.

KVO on webView.url fires for every URL change including SPA pushState,
covering the case where the app routes client-side after the initial
page load. Both KVO and didFinish now call the same checkCurrentURL
helper so detection is not missed regardless of navigation type.
2026-05-11 10:46:11 +02:00
github-actions[bot] 88252e4d59 Beta release v1.2.1-beta.10 2026-05-11 08:36:44 +00:00
superdooper86 ad2ff3a7b6 fix: revert to URL-based auth detection; remove browser headers
Every JS/cookie-based detection approach failed. Reverting to the
simplest reliable mechanism: if the WebView navigates to any non-login,
non-auth URL, the server redirected us after sign-in — fire onAuthenticated.

Also removing the browser headers added in beta.6. The 1.1.4 version
worked without them and they may be triggering server-side bot detection.
All-cookies approach (beta.8) is kept.
2026-05-11 10:35:30 +02:00
github-actions[bot] 1a9d9cd22a Beta release v1.2.1-beta.9 2026-05-11 08:23:48 +00:00
superdooper86 b8826ace9b fix: NSNumber cast and use .page world in callAsyncJavaScript auth check
callAsyncJavaScript returns JS numbers as NSNumber (Double-backed).
'val as? Int' silently returns nil for 200.0, so onAuthenticated never
fired. Fixed with 'val as? NSNumber then .intValue == 200'.

Also switched content world from .defaultClient to .page so the fetch
runs in the same JS context as the loaded page.
2026-05-11 10:22:59 +02:00
github-actions[bot] 2a0de269ff Beta release v1.2.1-beta.8 2026-05-11 08:07:51 +00:00
7 changed files with 155 additions and 89 deletions
+1 -1
View File
@@ -117,7 +117,7 @@ struct ContentView: View {
} }
.sheet(isPresented: $showLogin) { .sheet(isPresented: $showLogin) {
LoginSheetView(isPresented: $showLogin) { LoginSheetView(isPresented: $showLogin) {
Task { await vm.refresh() } Task { await vm.reloadAPIWebViewAndRefresh() }
} }
} }
.sheet(isPresented: $showUpdateSheet) { .sheet(isPresented: $showUpdateSheet) {
+2 -2
View File
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.2.1-beta.8</string> <string>1.2.1-beta.16</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>56</string> <string>64</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>13.0</string> <string>13.0</string>
<key>LSUIElement</key> <key>LSUIElement</key>
+21 -20
View File
@@ -12,6 +12,12 @@ struct LoginWebView: NSViewRepresentable {
let webView = WKWebView(frame: .zero, configuration: config) let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = context.coordinator webView.navigationDelegate = context.coordinator
// KVO on url catches SPA pushState navigations that don't fire didFinish
context.coordinator.urlObservation = webView.observe(\.url, options: [.new]) { [weak coordinator = context.coordinator] wv, _ in
coordinator?.checkCurrentURL(wv.url?.absoluteString)
}
webView.load(URLRequest(url: URL(string: "https://claude.ai/login")!)) webView.load(URLRequest(url: URL(string: "https://claude.ai/login")!))
return webView return webView
} }
@@ -25,33 +31,28 @@ struct LoginWebView: NSViewRepresentable {
class Coordinator: NSObject, WKNavigationDelegate { class Coordinator: NSObject, WKNavigationDelegate {
let onAuthenticated: () -> Void let onAuthenticated: () -> Void
var didAuthenticate = false var didAuthenticate = false
var urlObservation: NSKeyValueObservation?
init(onAuthenticated: @escaping () -> Void) { init(onAuthenticated: @escaping () -> Void) {
self.onAuthenticated = onAuthenticated self.onAuthenticated = onAuthenticated
} }
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { func checkCurrentURL(_ url: String?) {
guard !didAuthenticate else { return } guard !didAuthenticate, let url else { return }
// Don't fire on the login/auth pages themselves // Ignore navigations to external OAuth providers (Google, etc.)
if let url = webView.url?.absoluteString, // only consider auth complete when we land back on claude.ai/anthropic.com
url.contains("/login") || url.contains("/auth") { return } guard url.contains("claude.ai") || url.contains("anthropic.com") else { return }
if url.contains("/login") || url.contains("/auth") { return }
// Ask the WebView itself whether we're authenticated it uses its own didAuthenticate = true
// session (cookies, localStorage, etc.) so we don't need to know the DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// cookie domain or name. self.onAuthenticated()
webView.callAsyncJavaScript(
"const r = await fetch('/api/bootstrap', {credentials: 'include'}); return r.status;",
arguments: [:], in: nil, in: .defaultClient
) { [weak self] result in
guard let self, !self.didAuthenticate else { return }
if case .success(let val) = result, let status = val as? Int, status == 200 {
self.didAuthenticate = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.onAuthenticated()
}
}
} }
} }
// Covers full cross-document navigations
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
checkCurrentURL(webView.url?.absoluteString)
}
} }
} }
+122 -53
View File
@@ -30,6 +30,15 @@ class UsageViewModel: ObservableObject {
private var previousPercents: [String: Double] = [:] private var previousPercents: [String: Double] = [:]
private var firedThresholds: [String: Set<Int>] = [:] private var firedThresholds: [String: Set<Int>] = [:]
// Background WKWebView for API calls via callAsyncJavaScript.
// Hosted in a hidden NSWindow without a window WebKit suspends the WebView
// and JS execution stops working, breaking callAsyncJavaScript.
private var apiWebView: WKWebView?
private var apiDelegate: APIWebViewDelegate?
private var apiWindow: NSWindow?
private var apiWebViewLoaded = false
private var apiReadyContinuations: [CheckedContinuation<Void, Never>] = []
init() { init() {
let saved = UserDefaults.standard.double(forKey: "refresh_interval") let saved = UserDefaults.standard.double(forKey: "refresh_interval")
refreshInterval = saved > 0 ? saved : 60 refreshInterval = saved > 0 ? saved : 60
@@ -38,9 +47,85 @@ class UsageViewModel: ObservableObject {
burnHistoryStore = saved burnHistoryStore = saved
} }
loadPlaceholderData() loadPlaceholderData()
setupAPIWebView()
Task { await checkInitialSignInState() } Task { await checkInitialSignInState() }
} }
// MARK: - Background API WebView
private func setupAPIWebView() {
let config = WKWebViewConfiguration()
config.websiteDataStore = WKWebsiteDataStore.default()
let wv = WKWebView(frame: NSRect(x: 0, y: 0, width: 1, height: 1), configuration: config)
let del = APIWebViewDelegate()
del.onNavigationEnd = { [weak self] in
guard let self else { return }
self.apiWebViewLoaded = true
self.resumeAPIReadyContinuations()
}
wv.navigationDelegate = del
apiWebView = wv
apiDelegate = del
// A WKWebView with no window is suspended by macOS JS execution won't run.
// Hosting it in a 1×1 transparent window keeps WebKit's process alive.
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1, height: 1),
styleMask: .borderless,
backing: .buffered,
defer: false)
window.alphaValue = 0.0
window.ignoresMouseEvents = true
window.isReleasedWhenClosed = false
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]
window.contentView?.addSubview(wv)
window.orderFrontRegardless()
apiWindow = window
wv.load(URLRequest(url: URL(string: "https://claude.ai")!))
}
private func resumeAPIReadyContinuations() {
let pending = apiReadyContinuations
apiReadyContinuations.removeAll()
pending.forEach { $0.resume() }
}
private func waitForAPIWebViewReady() async {
guard !apiWebViewLoaded else { return }
await withCheckedContinuation { cont in
apiReadyContinuations.append(cont)
}
}
// Called after login: reloads the background WebView to pick up the new session.
func reloadAPIWebViewAndRefresh() async {
guard let wv = apiWebView else {
await refresh()
return
}
apiWebViewLoaded = false
wv.load(URLRequest(url: URL(string: "https://claude.ai")!))
await waitForAPIWebViewReady()
try? await Task.sleep(nanoseconds: 500_000_000)
await refresh()
}
// Executes a same-origin fetch inside the background WebView's page context.
// This includes all credentials the page has (cookies, localStorage tokens, etc.),
// which URLSession cannot access hence using callAsyncJavaScript instead.
private func webViewFetch(_ path: String) async throws -> (statusCode: Int, body: String) {
guard let wv = apiWebView else { throw AppError.networkError }
await waitForAPIWebViewReady()
let js = "const r = await fetch(path, {credentials:'include'}); return {s: r.status, b: await r.text()};"
let result = try await wv.callAsyncJavaScript(
js, arguments: ["path": path], in: nil, in: .page)
guard let d = result as? [String: Any],
let s = (d["s"] as? NSNumber)?.intValue,
let b = d["b"] as? String else { throw AppError.networkError }
return (s, b)
}
private func checkInitialSignInState() async { private func checkInitialSignInState() async {
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies() let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
if !cookies.isEmpty { isSignedIn = true } if !cookies.isEmpty { isSignedIn = true }
@@ -61,6 +146,8 @@ class UsageViewModel: ObservableObject {
extraUsage = nil extraUsage = nil
prepaidCredits = nil prepaidCredits = nil
overageSpendLimit = nil overageSpendLimit = nil
apiWebViewLoaded = false
apiWebView?.load(URLRequest(url: URL(string: "https://claude.ai")!))
} }
func refresh() async { func refresh() async {
@@ -126,50 +213,29 @@ class UsageViewModel: ObservableObject {
} }
} }
// MARK: - Bootstrap (org ID + email + plan label in one call) // MARK: - Bootstrap
private func claudeAPIRequest(for url: URL) async -> URLRequest {
var req = URLRequest(url: url)
req.setValue("application/json, text/plain, */*", forHTTPHeaderField: "accept")
req.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
req.setValue("https://claude.ai", forHTTPHeaderField: "Origin")
req.setValue("https://claude.ai/", forHTTPHeaderField: "Referer")
req.setValue("same-origin", forHTTPHeaderField: "sec-fetch-site")
req.setValue("cors", forHTTPHeaderField: "sec-fetch-mode")
req.setValue("empty", forHTTPHeaderField: "sec-fetch-dest")
if let cookie = await claudeCookieHeader() {
req.setValue(cookie, forHTTPHeaderField: "Cookie")
}
return req
}
private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) { private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) {
let url = URL(string: "https://claude.ai/api/bootstrap")! let (status, body) = try await webViewFetch("/api/bootstrap")
var req = await claudeAPIRequest(for: url) if status == 401 || status == 403 { throw AppError.notAuthenticated }
guard req.value(forHTTPHeaderField: "Cookie") != nil else { throw AppError.notAuthenticated } guard status == 200 else { throw AppError.networkError }
let (data, response) = try await URLSession.shared.data(for: req) guard let data = body.data(using: .utf8),
guard let http = response as? HTTPURLResponse else { throw AppError.networkError } let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated }
guard http.statusCode == 200 else { throw AppError.networkError }
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return (nil, nil, nil) return (nil, nil, nil)
} }
let account = json["account"] as? [String: Any] let account = json["account"] as? [String: Any]
// memberships may live under account or at root (older API shape)
let memberships = (account?["memberships"] ?? json["memberships"]) as? [[String: Any]] let memberships = (account?["memberships"] ?? json["memberships"]) as? [[String: Any]]
let firstOrg = memberships?.first?["organization"] as? [String: Any] let firstOrg = memberships?.first?["organization"] as? [String: Any]
// org ID primary path then flat-list fallback then dedicated endpoint
var orgId: String? = firstOrg?["uuid"] as? String var orgId: String? = firstOrg?["uuid"] as? String
if orgId == nil { if orgId == nil {
orgId = (json["organizations"] as? [[String: Any]])?.first?["uuid"] as? String orgId = (json["organizations"] as? [[String: Any]])?.first?["uuid"] as? String
} }
if orgId == nil { if orgId == nil {
// Final fallback: fetch /api/organizations directly if let (orgsStatus, orgsBody) = try? await webViewFetch("/api/organizations"),
let orgsReq = await claudeAPIRequest(for: URL(string: "https://claude.ai/api/organizations")!) orgsStatus == 200,
if let (orgsData, orgsResp) = try? await URLSession.shared.data(for: orgsReq), let orgsData = orgsBody.data(using: .utf8),
let orgsHttp = orgsResp as? HTTPURLResponse, orgsHttp.statusCode == 200,
let orgs = try? JSONSerialization.jsonObject(with: orgsData) as? [[String: Any]] { let orgs = try? JSONSerialization.jsonObject(with: orgsData) as? [[String: Any]] {
orgId = orgs.first?["uuid"] as? String orgId = orgs.first?["uuid"] as? String
} }
@@ -177,7 +243,6 @@ class UsageViewModel: ObservableObject {
let email = account?["email_address"] as? String let email = account?["email_address"] as? String
// plan label from capabilities e.g. "claude_pro" -> "Pro"
var planLabel: String? = nil var planLabel: String? = nil
if let caps = firstOrg?["capabilities"] as? [String], if let caps = firstOrg?["capabilities"] as? [String],
let cap = caps.first(where: { $0.hasPrefix("claude_") }) { let cap = caps.first(where: { $0.hasPrefix("claude_") }) {
@@ -190,37 +255,25 @@ class UsageViewModel: ObservableObject {
// MARK: - Fetch usage // MARK: - Fetch usage
private func claudeCookieHeader() async -> String? {
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
guard !cookies.isEmpty else { return nil }
// Send all cookies from the app's WebView store the session token may be
// on any domain (claude.ai, anthropic.com, or an auth sub-service).
return HTTPCookie.requestHeaderFields(with: cookies)["Cookie"]
}
private func fetchUsage(orgId: String) async throws -> UsageResponse { private func fetchUsage(orgId: String) async throws -> UsageResponse {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/usage")! let (status, body) = try await webViewFetch("/api/organizations/\(orgId)/usage")
let req = await claudeAPIRequest(for: url) if status == 401 || status == 403 { throw AppError.notAuthenticated }
let (data, response) = try await URLSession.shared.data(for: req) guard status == 200 else { throw AppError.networkError }
guard let http = response as? HTTPURLResponse else { throw AppError.networkError } guard let data = body.data(using: .utf8) else { throw AppError.networkError }
if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated }
guard http.statusCode == 200 else { throw AppError.networkError }
return try JSONDecoder().decode(UsageResponse.self, from: data) return try JSONDecoder().decode(UsageResponse.self, from: data)
} }
private func fetchPrepaidCredits(orgId: String) async throws -> PrepaidCredits? { private func fetchPrepaidCredits(orgId: String) async throws -> PrepaidCredits? {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/prepaid/credits")! guard let (status, body) = try? await webViewFetch("/api/organizations/\(orgId)/prepaid/credits"),
let req = await claudeAPIRequest(for: url) status == 200,
let (data, response) = try await URLSession.shared.data(for: req) let data = body.data(using: .utf8) else { return nil }
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
return try? JSONDecoder().decode(PrepaidCredits.self, from: data) return try? JSONDecoder().decode(PrepaidCredits.self, from: data)
} }
private func fetchOverageSpendLimit(orgId: String) async throws -> OverageSpendLimit? { private func fetchOverageSpendLimit(orgId: String) async throws -> OverageSpendLimit? {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/overage_spend_limit")! guard let (status, body) = try? await webViewFetch("/api/organizations/\(orgId)/overage_spend_limit"),
let req = await claudeAPIRequest(for: url) status == 200,
let (data, response) = try await URLSession.shared.data(for: req) let data = body.data(using: .utf8) else { return nil }
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
return try? JSONDecoder().decode(OverageSpendLimit.self, from: data) return try? JSONDecoder().decode(OverageSpendLimit.self, from: data)
} }
@@ -337,6 +390,22 @@ class UsageViewModel: ObservableObject {
} }
} }
// MARK: - API WebView Delegate
private class APIWebViewDelegate: NSObject, WKNavigationDelegate {
var onNavigationEnd: (() -> Void)?
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
onNavigationEnd?()
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
onNavigationEnd?()
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
onNavigationEnd?()
}
}
enum AppError: LocalizedError { enum AppError: LocalizedError {
case notAuthenticated case notAuthenticated
case networkError case networkError
+1 -1
View File
@@ -10,7 +10,7 @@
[![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org) [![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-1.2.0-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases) [![Version](https://img.shields.io/badge/version-1.2.0-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases)
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE) [![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
[![Beta](https://img.shields.io/badge/beta-1.2.1--beta.7-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.7) <!-- BETA_BADGE --> [![Beta](https://img.shields.io/badge/beta-1.2.1--beta.15-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.15) <!-- BETA_BADGE -->
</div> </div>
+5 -9
View File
@@ -1,12 +1,8 @@
## What's new in v1.2.1 ## What's new in v1.2.1
### Bug fixes ### Bug fixes
- Fixed "Not signed in" showing incorrectly on launch when the session was already active - Fixed the root cause of "Not signed in" after being clearly signed in: claude.ai's auth requires credentials beyond plain HTTP cookies (localStorage tokens, Service Worker state, etc.) that URLSession cannot access. All API calls now run via callAsyncJavaScript inside a background WKWebView, using the same fetch path the page itself uses — credentials are included automatically.
- Sign-in state is now detected immediately from stored cookies on startup, before the first data refresh completes - Fixed WebKit suspending the background WKWebView: a WKWebView with no window is throttled/suspended by macOS, preventing JS execution. The background WebView is now anchored in a transparent 1×1 NSWindow, keeping it active.
- Fixed login window auto-closing before the user could sign in — the login window now correctly loads the `/login` page so it only detects auth after the actual sign-in redirect - Fixed login window auto-closing before sign-in completes — login window loads `/login` and only detects auth when back on claude.ai (not on OAuth provider redirects)
- Fixed "No API key configured" showing after signing out — now correctly shows "Not signed in" with a prompt to sign in - Added `/api/organizations` as a final fallback for org ID resolution
- Added `/api/organizations` as a final fallback for org ID resolution when the bootstrap API response doesn't include it - Fixed Settings incorrectly showing "Signed in" after a failed refresh
- Fixed usage data not loading — API requests now include required browser-like headers (Origin, Referer, User-Agent)
- Fixed Settings incorrectly showing "Signed in" after a failed refresh — sign-in state now resets when authentication fails
- Rewrote login detection to use the WebView's own fetch call instead of inspecting cookie domains — correctly detects auth regardless of which domain the session token is stored on
- Fixed API requests not including session cookies — now sends all cookies from the app's WebView store rather than filtering by domain
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.2.1-beta.7", "version": "1.2.1-beta.15",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.7/ClaudeChecker.zip", "url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.15/ClaudeChecker.zip",
"notes": "## What's new in v1.2.1\n\n### Bug fixes\n- Fixed \"Not signed in\" showing incorrectly on launch when the session was already active\n- Sign-in state is now detected immediately from stored cookies on startup, before the first data refresh completes\n- Fixed login window auto-closing before the user could sign in — the login window now correctly loads the `/login` page so it only detects auth after the actual sign-in redirect\n- Fixed \"No API key configured\" showing after signing out — now correctly shows \"Not signed in\" with a prompt to sign in\n- Added `/api/organizations` as a final fallback for org ID resolution when the bootstrap API response doesn't include it\n- Fixed usage data not loading — API requests now include required browser-like headers (Origin, Referer, User-Agent)\n- Fixed sign-in detection and cookie handling for accounts whose session cookies are on the `anthropic.com` domain rather than `claude.ai`\n- Fixed Settings incorrectly showing \"Signed in\" after a failed refresh — sign-in state now resets when authentication fails" "notes": "## What's new in v1.2.1\n\n### Bug fixes\n- Fixed the root cause of \"Not signed in\" errors: URLSession was silently discarding the manually-set Cookie header because `httpShouldHandleCookies` defaults to `true`, which makes URLSession replace it with its own (empty) HTTPCookieStorage — claude.ai session cookies live in WKWebsiteDataStore, not HTTPCookieStorage. Setting `httpShouldHandleCookies = false` ensures the cookies are actually sent.\n- Added browser-like request headers (User-Agent, Origin, Referer) matching what Claude's API expects, consistent with the working Windows implementation\n- Removed background WKWebView complexity added in beta.1213 — reverted to simple URLSession approach with correct cookie handling\n- Fixed login window auto-closing before the user could sign in — login window loads `/login` so auth is only detected after the actual sign-in redirect\n- Fixed login detection for Next.js SPA navigation using KVO on WebView URL (history.pushState doesn't trigger didFinish)\n- Added `/api/organizations` as a final fallback for org ID resolution\n- Fixed Settings incorrectly showing \"Signed in\" after a failed refresh"
} }