RedditOAuthPanelController.swift 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import AppKit
  2. import WebKit
  3. @MainActor
  4. final class RedditOAuthPanelController: NSObject {
  5. static let shared = RedditOAuthPanelController()
  6. private var panel: NSPanel?
  7. private var webView: WKWebView?
  8. private var onComplete: (() -> Void)?
  9. private var onAuthError: ((String) -> Void)?
  10. private var finishWorkItem: DispatchWorkItem?
  11. private override init() {
  12. super.init()
  13. }
  14. func completeSignIn(
  15. callbackURL: URL,
  16. onAuthError: @escaping (String) -> Void,
  17. onFinished: @escaping () -> Void
  18. ) {
  19. dismiss()
  20. self.onComplete = onFinished
  21. self.onAuthError = onAuthError
  22. let panel = NSPanel(
  23. contentRect: NSRect(x: 0, y: 0, width: 520, height: 640),
  24. styleMask: [.titled, .closable, .fullSizeContentView],
  25. backing: .buffered,
  26. defer: false
  27. )
  28. panel.title = "Sign in to Reddit"
  29. panel.isReleasedWhenClosed = false
  30. panel.center()
  31. panel.level = .floating
  32. let webView = WKWebView(frame: panel.contentView?.bounds ?? .zero, configuration: RedditWebViewManager.makeConfiguration())
  33. webView.navigationDelegate = self
  34. webView.uiDelegate = self
  35. webView.autoresizingMask = [.width, .height]
  36. panel.contentView?.addSubview(webView)
  37. self.panel = panel
  38. self.webView = webView
  39. panel.makeKeyAndOrderFront(nil)
  40. NSApp.activate(ignoringOtherApps: true)
  41. webView.load(URLRequest(url: callbackURL))
  42. }
  43. func dismiss() {
  44. finishWorkItem?.cancel()
  45. finishWorkItem = nil
  46. panel?.close()
  47. panel = nil
  48. webView = nil
  49. onComplete = nil
  50. onAuthError = nil
  51. }
  52. private func finish() {
  53. let completion = onComplete
  54. dismiss()
  55. completion?()
  56. }
  57. private func scheduleFinishIfNeeded(for url: URL) {
  58. guard RedditWebAuthHelper.shouldCompleteOAuthSignIn(for: url) else { return }
  59. finishWorkItem?.cancel()
  60. let workItem = DispatchWorkItem { [weak self] in
  61. self?.finish()
  62. }
  63. finishWorkItem = workItem
  64. DispatchQueue.main.asyncAfter(deadline: .now() + 0.75, execute: workItem)
  65. }
  66. private func beginOAuthSignIn(url: URL, in webView: WKWebView) {
  67. OAuthAuthenticationManager.shared.authenticate(
  68. url: url,
  69. presentationWindow: panel
  70. ) { [weak self] result in
  71. guard let self else { return }
  72. switch result {
  73. case .success(let callbackURL):
  74. webView.load(URLRequest(url: callbackURL))
  75. case .failure(let error as OAuthError) where error == .cancelled:
  76. break
  77. case .failure(let error):
  78. self.onAuthError?(error.localizedDescription)
  79. self.finish()
  80. }
  81. }
  82. }
  83. }
  84. extension RedditOAuthPanelController: WKNavigationDelegate {
  85. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  86. guard let url = webView.url else { return }
  87. scheduleFinishIfNeeded(for: url)
  88. }
  89. func webView(
  90. _ webView: WKWebView,
  91. decidePolicyFor navigationAction: WKNavigationAction,
  92. decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
  93. ) {
  94. guard let url = navigationAction.request.url else {
  95. decisionHandler(.allow)
  96. return
  97. }
  98. if RedditWebAuthHelper.isOAuthProviderURL(url) {
  99. decisionHandler(.cancel)
  100. beginOAuthSignIn(url: url, in: webView)
  101. return
  102. }
  103. decisionHandler(.allow)
  104. }
  105. }
  106. extension RedditOAuthPanelController: WKUIDelegate {
  107. func webView(
  108. _ webView: WKWebView,
  109. createWebViewWith configuration: WKWebViewConfiguration,
  110. for navigationAction: WKNavigationAction,
  111. windowFeatures: WKWindowFeatures
  112. ) -> WKWebView? {
  113. guard let url = navigationAction.request.url else { return nil }
  114. if RedditWebAuthHelper.isOAuthProviderURL(url) {
  115. beginOAuthSignIn(url: url, in: webView)
  116. return nil
  117. }
  118. webView.load(URLRequest(url: url))
  119. return nil
  120. }
  121. }