RedditOAuthPanelController.swift 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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.uiDelegate = self
  34. webView.autoresizingMask = [.width, .height]
  35. panel.contentView?.addSubview(webView)
  36. self.panel = panel
  37. self.webView = webView
  38. panel.makeKeyAndOrderFront(nil)
  39. NSApp.activate(ignoringOtherApps: true)
  40. webView.load(URLRequest(url: callbackURL))
  41. }
  42. func dismiss() {
  43. panel?.close()
  44. panel = nil
  45. webView = nil
  46. onComplete = nil
  47. onAuthError = nil
  48. }
  49. private func finish() {
  50. let completion = onComplete
  51. dismiss()
  52. completion?()
  53. }
  54. private func beginOAuthSignIn(url: URL, in webView: WKWebView) {
  55. OAuthAuthenticationManager.shared.authenticate(
  56. url: url,
  57. presentationWindow: panel
  58. ) { [weak self] result in
  59. guard let self else { return }
  60. switch result {
  61. case .success(let callbackURL):
  62. webView.load(URLRequest(url: callbackURL))
  63. case .failure(let error):
  64. self.onAuthError?(error.localizedDescription)
  65. self.finish()
  66. }
  67. }
  68. }
  69. }
  70. extension RedditOAuthPanelController: WKNavigationDelegate {
  71. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  72. guard let url = webView.url, RedditWebAuthHelper.isRedditURL(url) else { return }
  73. if shouldFinish(for: url) {
  74. finish()
  75. }
  76. }
  77. func webView(
  78. _ webView: WKWebView,
  79. decidePolicyFor navigationAction: WKNavigationAction,
  80. decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
  81. ) {
  82. guard let url = navigationAction.request.url else {
  83. decisionHandler(.allow)
  84. return
  85. }
  86. if RedditWebAuthHelper.isOAuthProviderURL(url) {
  87. decisionHandler(.cancel)
  88. beginOAuthSignIn(url: url, in: webView)
  89. return
  90. }
  91. if shouldFinish(for: url) {
  92. decisionHandler(.allow)
  93. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
  94. self?.finish()
  95. }
  96. return
  97. }
  98. decisionHandler(.allow)
  99. }
  100. private func shouldFinish(for url: URL) -> Bool {
  101. guard let host = url.host?.lowercased() else { return false }
  102. let path = url.path.lowercased()
  103. if host == "www.reddit.com" || host == "reddit.com" {
  104. return !path.contains("login") && !path.contains("register")
  105. }
  106. return RedditWebAuthHelper.looksLikeLoggedInURL(url)
  107. }
  108. }
  109. extension RedditOAuthPanelController: WKUIDelegate {
  110. func webView(
  111. _ webView: WKWebView,
  112. createWebViewWith configuration: WKWebViewConfiguration,
  113. for navigationAction: WKNavigationAction,
  114. windowFeatures: WKWindowFeatures
  115. ) -> WKWebView? {
  116. guard let url = navigationAction.request.url else { return nil }
  117. if RedditWebAuthHelper.isOAuthProviderURL(url) {
  118. beginOAuthSignIn(url: url, in: webView)
  119. return nil
  120. }
  121. webView.load(URLRequest(url: url))
  122. return nil
  123. }
  124. }