Files
claudechecker/ClaudeChecker/LoginView.swift
T

103 lines
3.5 KiB
Swift

import SwiftUI
import WebKit
// MARK: - Login Web View
struct LoginWebView: NSViewRepresentable {
let onAuthenticated: () -> Void
func makeNSView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
config.websiteDataStore = WKWebsiteDataStore.default()
let webView = WKWebView(frame: .zero, configuration: config)
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")!))
return webView
}
func updateNSView(_ nsView: WKWebView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(onAuthenticated: onAuthenticated)
}
class Coordinator: NSObject, WKNavigationDelegate {
let onAuthenticated: () -> Void
var didAuthenticate = false
var urlObservation: NSKeyValueObservation?
init(onAuthenticated: @escaping () -> Void) {
self.onAuthenticated = onAuthenticated
}
func checkCurrentURL(_ url: String?) {
guard !didAuthenticate, let url else { return }
// Ignore navigations to external OAuth providers (Google, etc.) —
// only consider auth complete when we land back on claude.ai/anthropic.com
guard url.contains("claude.ai") || url.contains("anthropic.com") else { return }
if url.contains("/login") || url.contains("/auth") { return }
didAuthenticate = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.onAuthenticated()
}
}
// Covers full cross-document navigations
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
checkCurrentURL(webView.url?.absoluteString)
}
}
}
// MARK: - Login Sheet View
struct LoginSheetView: View {
@Binding var isPresented: Bool
let onDone: () -> Void
@State private var authenticated = false
var body: some View {
VStack(spacing: 0) {
HStack {
Text(authenticated ? "✓ Signed in — loading data…" : "Sign in to Claude")
.font(.system(size: 13, weight: .semibold))
Spacer()
if authenticated {
Button("Done") {
isPresented = false
onDone()
}
.buttonStyle(.borderedProminent)
.controlSize(.small)
} else {
Button("Cancel") { isPresented = false }
.buttonStyle(.bordered)
.controlSize(.small)
}
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(Color(nsColor: .windowBackgroundColor))
Divider()
LoginWebView {
authenticated = true
// Auto-dismiss and refresh after brief delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
isPresented = false
onDone()
}
}
}
.frame(width: 460, height: 560)
}
}