RedditOAuthPanelController.swift 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  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 override init() {
  10. super.init()
  11. }
  12. func completeSignIn(
  13. callbackURL: URL,
  14. onFinished: @escaping () -> Void
  15. ) {
  16. dismiss()
  17. onComplete = onFinished
  18. let panel = NSPanel(
  19. contentRect: NSRect(x: 0, y: 0, width: 520, height: 640),
  20. styleMask: [.titled, .closable, .fullSizeContentView],
  21. backing: .buffered,
  22. defer: false
  23. )
  24. panel.title = "Sign in to Reddit"
  25. panel.isReleasedWhenClosed = false
  26. panel.center()
  27. panel.level = .floating
  28. let webView = WKWebView(frame: panel.contentView?.bounds ?? .zero, configuration: RedditWebViewManager.makeConfiguration())
  29. webView.navigationDelegate = self
  30. webView.autoresizingMask = [.width, .height]
  31. panel.contentView?.addSubview(webView)
  32. self.panel = panel
  33. self.webView = webView
  34. panel.makeKeyAndOrderFront(nil)
  35. NSApp.activate(ignoringOtherApps: true)
  36. webView.load(URLRequest(url: callbackURL))
  37. }
  38. func dismiss() {
  39. panel?.close()
  40. panel = nil
  41. webView = nil
  42. onComplete = nil
  43. }
  44. private func finish() {
  45. let completion = onComplete
  46. dismiss()
  47. completion?()
  48. }
  49. }
  50. extension RedditOAuthPanelController: WKNavigationDelegate {
  51. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  52. guard let url = webView.url, RedditWebAuthHelper.isRedditURL(url) else { return }
  53. if shouldFinish(for: url) {
  54. finish()
  55. return
  56. }
  57. if url.host?.lowercased() == "accounts.reddit.com" {
  58. DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
  59. guard let self, self.panel != nil else { return }
  60. self.finish()
  61. }
  62. }
  63. }
  64. func webView(
  65. _ webView: WKWebView,
  66. decidePolicyFor navigationAction: WKNavigationAction,
  67. decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
  68. ) {
  69. guard let url = navigationAction.request.url else {
  70. decisionHandler(.allow)
  71. return
  72. }
  73. if RedditWebAuthHelper.isOAuthProviderURL(url) {
  74. decisionHandler(.cancel)
  75. OAuthAuthenticationManager.shared.authenticate(
  76. url: url,
  77. presentationWindow: panel
  78. ) { [weak self] result in
  79. guard let self else { return }
  80. switch result {
  81. case .success(let callbackURL):
  82. webView.load(URLRequest(url: callbackURL))
  83. case .failure:
  84. self.finish()
  85. }
  86. }
  87. return
  88. }
  89. if shouldFinish(for: url) {
  90. decisionHandler(.allow)
  91. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
  92. self?.finish()
  93. }
  94. return
  95. }
  96. decisionHandler(.allow)
  97. }
  98. private func shouldFinish(for url: URL) -> Bool {
  99. guard let host = url.host?.lowercased() else { return false }
  100. let path = url.path.lowercased()
  101. if host == "www.reddit.com" || host == "reddit.com" {
  102. return !path.contains("login") && !path.contains("register")
  103. }
  104. return RedditWebAuthHelper.looksLikeLoggedInURL(url)
  105. }
  106. }