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.
100 lines
3.2 KiB
Swift
100 lines
3.2 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 }
|
|
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)
|
|
}
|
|
}
|