import AppKit import SwiftUI struct OAuthSetupView: View { @State private var tokenManager = TokenManager.shared @State private var statusMessage: String? var body: some View { VStack(spacing: 16) { Image(systemName: "link.circle") .font(.system(size: 40)) .foregroundStyle(.blue) Text("Connect Feishu") .font(.headline) Text("Authorize Bugger to read bugs assigned to you from your Feishu Bitable.") .font(.caption) .foregroundStyle(.secondary) .multilineTextAlignment(.center) if case .authenticating = tokenManager.state { ProgressView("Completing sign-in...") } else if tokenManager.authorizeURL == nil { Text("Set FEISHU_APP_ID and FEISHU_APP_SECRET in the Xcode build settings before connecting.") .font(.caption) .foregroundStyle(.orange) .multilineTextAlignment(.center) } else { Button("Connect Feishu") { connectFeishu() } .buttonStyle(.borderedProminent) .disabled(statusMessage != nil) } if case .error(let message) = tokenManager.state { VStack(spacing: 4) { Text("Authorization failed") .font(.caption) .fontWeight(.medium) .foregroundStyle(.red) Text(message) .font(.caption2) .foregroundStyle(.secondary) .multilineTextAlignment(.center) Button("Try again") { tokenManager.clearTokens() statusMessage = nil } .font(.caption) } } if let statusMessage, !statusMessage.isEmpty, tokenManager.state != .authenticating { Text(statusMessage) .font(.caption) .foregroundStyle(.secondary) } } .padding(24) .frame(maxWidth: .infinity, maxHeight: .infinity) } private func connectFeishu() { statusMessage = "Starting..." Task { let server = LocalOAuthServer(port: 18_923) tokenManager.authService.redirectURI = server.redirectURI guard let url = tokenManager.authorizeURL else { statusMessage = nil tokenManager.setError("Missing credentials. Check FEISHU_APP_ID.") return } statusMessage = "Complete authorization in your browser..." NSWorkspace.shared.open(url) do { let code = try await server.receiveCode() statusMessage = "Completing sign-in..." try await tokenManager.handleCallback(code: code) statusMessage = nil await PollerService.shared.startIfConfigured() await PollerService.shared.fetchNow() } catch { statusMessage = nil tokenManager.setError(error.localizedDescription) } } } }