Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions Sources/CodexBar/Providers/Claude/ClaudeLoginFlow.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import CodexBarCore
import Foundation

typealias ClaudeLoginFlowRunner = (
_ timeout: TimeInterval,
_ onPhaseChange: @escaping @Sendable (ClaudeLoginRunner.Phase) -> Void) async -> ClaudeLoginRunner.Result

enum ClaudeLoginFlowPolicy {
static func usageDataSourceAfterSuccessfulLogin(previous: ClaudeUsageDataSource) -> ClaudeUsageDataSource {
previous
}
}

@MainActor
extension StatusItemController {
func runClaudeLoginFlow() async -> Bool {
await self.runClaudeLoginFlow(
loginRunner: { timeout, onPhaseChange in
await ClaudeLoginRunner.run(timeout: timeout, onPhaseChange: onPhaseChange)
})
}

func runClaudeLoginFlow(loginRunner: ClaudeLoginFlowRunner) async -> Bool {
let phaseHandler: @Sendable (ClaudeLoginRunner.Phase) -> Void = { [weak self] phase in
Task { @MainActor in
switch phase {
Expand All @@ -11,7 +29,7 @@ extension StatusItemController {
}
}
}
let result = await ClaudeLoginRunner.run(timeout: 120, onPhaseChange: phaseHandler)
let result = await loginRunner(120, phaseHandler)
guard !Task.isCancelled else { return false }
self.loginPhase = .idle
self.presentClaudeLoginResult(result)
Expand All @@ -21,7 +39,8 @@ extension StatusItemController {
if case .success = result.outcome {
let metadata = self.store.metadata(for: .claude)
self.settings.setProviderEnabled(provider: .claude, metadata: metadata, enabled: true)
self.settings.claudeUsageDataSource = .oauth
self.settings.claudeUsageDataSource = ClaudeLoginFlowPolicy.usageDataSourceAfterSuccessfulLogin(
previous: self.settings.claudeUsageDataSource)
self.postLoginNotification(for: .claude)
return true
}
Expand Down
55 changes: 55 additions & 0 deletions Tests/CodexBarTests/ClaudeLoginFlowPolicyTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import CodexBarCore
import Testing
@testable import CodexBar

@MainActor
@Suite(.serialized)
struct ClaudeLoginFlowPolicyTests {
@Test
func `successful Claude login preserves selected source so auto fallback remains available`() {
for source in ClaudeUsageDataSource.allCases {
#expect(ClaudeLoginFlowPolicy.usageDataSourceAfterSuccessfulLogin(previous: source) == source)
}
}

@Test
func `successful Claude login controller flow preserves selected source and enables provider`() async throws {
let registry = ProviderRegistry.shared
let claudeMetadata = try #require(registry.metadata[.claude])

for source in ClaudeUsageDataSource.allCases {
let settings = testSettingsStore(
suiteName: "ClaudeLoginFlowPolicyTests-controller-\(source.rawValue)")
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.providerDetectionCompleted = true
settings.claudeUsageDataSource = source
settings.setProviderEnabled(provider: .claude, metadata: claudeMetadata, enabled: false)

let fetcher = UsageFetcher()
let store = UsageStore(
fetcher: fetcher,
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings)

await withStatusItemControllerForTesting(store: store, settings: settings, fetcher: fetcher) { controller in
let didLogin = await controller.runClaudeLoginFlow { _, onPhaseChange in
onPhaseChange(.requesting)
await Task.yield()
onPhaseChange(.waitingBrowser)
await Task.yield()
return ClaudeLoginRunner.Result(
outcome: .success,
output: "Successfully logged in",
authLink: nil)
}

#expect(didLogin)
#expect(controller.loginPhase == .idle)
}

#expect(settings.claudeUsageDataSource == source)
#expect(settings.isProviderEnabledCached(provider: .claude, metadataByProvider: registry.metadata))
}
}
}
3 changes: 2 additions & 1 deletion docs/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ Admin API key setup:
- `seven_day_routines` / `seven_day_cowork` → Daily Routines extra window.
- Claude Design/Omelette keys are ignored because Claude Design shares the main Claude usage limit.
- `extra_usage` → Extra usage cost (monthly spend/limit).
- Successful OAuth login enables Claude and selects OAuth as the usage source.
- Successful OAuth login enables Claude and preserves the selected usage source. With the default Auto source, OAuth
remains preferred when readable, while CLI/Web fallback stays available when OAuth credentials are not usable.
- Plan inference: `subscriptionType` is preferred when present; `rate_limit_tier` falls back to
Max/Pro/Team/Enterprise.

Expand Down