| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- 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
- }
- }
|