import AppKit import WebKit @MainActor final class RedditOAuthPanelController: NSObject { static let shared = RedditOAuthPanelController() private var panel: NSPanel? private var webView: WKWebView? private var onComplete: (() -> Void)? private var onAuthError: ((String) -> Void)? private var finishWorkItem: DispatchWorkItem? var isPresenting: Bool { panel != nil } private override init() { super.init() } func completeSignIn( callbackURL: URL, parentWindow: NSWindow? = nil, onAuthError: @escaping (String) -> Void, onFinished: @escaping () -> Void ) { presentPanel( url: callbackURL, parentWindow: parentWindow, onAuthError: onAuthError, onFinished: onFinished ) } func presentLogin( parentWindow: NSWindow? = nil, onAuthError: @escaping (String) -> Void, onFinished: @escaping () -> Void ) { presentPanel( url: RedditWebAuthHelper.redditLoginURL, parentWindow: parentWindow, onAuthError: onAuthError, onFinished: onFinished ) } private func presentPanel( url: URL, parentWindow: NSWindow? = nil, onAuthError: @escaping (String) -> Void, onFinished: @escaping () -> Void ) { dismiss() onComplete = onFinished self.onAuthError = onAuthError let panel = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 520, height: 640), styleMask: [.titled, .closable, .fullSizeContentView], backing: .buffered, defer: false ) panel.title = "Sign in to Reddit" panel.isReleasedWhenClosed = false panel.delegate = self let webView = WKWebView( frame: panel.contentView?.bounds ?? .zero, configuration: RedditWebViewManager.makeConfiguration(includeLoginBridge: false) ) webView.navigationDelegate = self webView.uiDelegate = self webView.autoresizingMask = [.width, .height] panel.contentView?.addSubview(webView) self.panel = panel self.webView = webView if let parentWindow { parentWindow.beginSheet(panel) } else { panel.center() panel.level = .floating panel.makeKeyAndOrderFront(nil) } NSApp.activate(ignoringOtherApps: true) webView.load(URLRequest(url: url)) } func dismiss() { finishWorkItem?.cancel() finishWorkItem = nil if let panel { if let parent = panel.sheetParent { parent.endSheet(panel) } else { panel.close() } } panel = nil webView = nil onComplete = nil onAuthError = nil } private func finish() { let completion = onComplete dismiss() completion?() } private func scheduleFinishIfNeeded(for url: URL) { guard RedditWebAuthHelper.shouldCompleteOAuthSignIn(for: url) else { return } finishWorkItem?.cancel() let workItem = DispatchWorkItem { [weak self] in self?.finish() } finishWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + 0.75, execute: workItem) } private func beginOAuthSignIn(url: URL, in webView: WKWebView) { OAuthAuthenticationManager.shared.authenticate( url: url, presentationWindow: panel ) { [weak self] result in guard let self else { return } switch result { case .success(let callbackURL): webView.load(URLRequest(url: callbackURL)) case .failure(let error as OAuthError) where error == .cancelled: break case .failure(let error): self.onAuthError?(error.localizedDescription) self.finish() } } } } extension RedditOAuthPanelController: NSWindowDelegate { func windowWillClose(_ notification: Notification) { guard notification.object as? NSPanel === panel else { return } finishWorkItem?.cancel() finishWorkItem = nil panel = nil webView = nil onComplete = nil onAuthError = nil } } extension RedditOAuthPanelController: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { guard let url = webView.url else { return } scheduleFinishIfNeeded(for: url) } func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void ) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } if RedditWebAuthHelper.isOAuthProviderURL(url) { decisionHandler(.cancel) beginOAuthSignIn(url: url, in: webView) return } if RedditWebAuthHelper.shouldCompleteOAuthSignIn(for: url) { decisionHandler(.allow) scheduleFinishIfNeeded(for: url) return } decisionHandler(.allow) } } extension RedditOAuthPanelController: WKUIDelegate { func webView( _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures ) -> WKWebView? { guard let url = navigationAction.request.url else { return nil } if RedditWebAuthHelper.isOAuthProviderURL(url) { beginOAuthSignIn(url: url, in: webView) return nil } webView.load(URLRequest(url: url)) return nil } }