RedditOAuthPanelController.swift 3.6 KB

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