| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- import AppKit
- import AuthenticationServices
- import WebKit
- enum OAuthError: LocalizedError {
- case alreadyInProgress
- case failedToStart
- case cancelled
- case invalidCallback
- case unsupportedPlatform
- var errorDescription: String? {
- switch self {
- case .alreadyInProgress:
- "Sign-in is already in progress."
- case .failedToStart:
- "Could not start sign-in."
- case .cancelled:
- "Sign-in was cancelled."
- case .invalidCallback:
- "Sign-in did not return to Reddit."
- case .unsupportedPlatform:
- "Sign-in requires macOS 14.4 or later. Please update your system."
- }
- }
- }
- @MainActor
- final class OAuthAuthenticationManager: NSObject {
- static let shared = OAuthAuthenticationManager()
- private var authSession: ASWebAuthenticationSession?
- private var isAuthenticating = false
- private weak var presentationWindow: NSWindow?
- private override init() {
- super.init()
- }
- func authenticate(
- url: URL,
- presentationWindow: NSWindow? = nil,
- completion: @escaping (Result<URL, Error>) -> Void
- ) {
- guard !isAuthenticating else {
- completion(.failure(OAuthError.alreadyInProgress))
- return
- }
- if #available(macOS 14.4, *) {
- self.presentationWindow = presentationWindow
- startAuthenticationSession(url: url, completion: completion)
- } else {
- NSWorkspace.shared.open(url)
- completion(.failure(OAuthError.unsupportedPlatform))
- }
- }
- @available(macOS 14.4, *)
- private func startAuthenticationSession(
- url: URL,
- completion: @escaping (Result<URL, Error>) -> Void
- ) {
- isAuthenticating = true
- NSApp.activate(ignoringOtherApps: true)
- let callbackHost = RedditWebAuthHelper.oauthCallbackHost(for: url)
- let session = ASWebAuthenticationSession(
- url: url,
- callback: .https(host: callbackHost, path: "")
- ) { [weak self] callbackURL, error in
- Task { @MainActor in
- self?.finishAuthentication(
- callbackURL: callbackURL,
- error: error,
- completion: completion
- )
- }
- }
- session.prefersEphemeralWebBrowserSession = false
- session.presentationContextProvider = self
- authSession = session
- guard session.start() else {
- isAuthenticating = false
- authSession = nil
- presentationWindow = nil
- completion(.failure(OAuthError.failedToStart))
- return
- }
- }
- private func finishAuthentication(
- callbackURL: URL?,
- error: Error?,
- completion: @escaping (Result<URL, Error>) -> Void
- ) {
- isAuthenticating = false
- authSession = nil
- presentationWindow = nil
- if let error {
- let nsError = error as NSError
- if nsError.domain == ASWebAuthenticationSessionErrorDomain,
- nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
- completion(.failure(OAuthError.cancelled))
- } else {
- completion(.failure(error))
- }
- return
- }
- guard let callbackURL, RedditWebAuthHelper.isRedditURL(callbackURL) else {
- completion(.failure(OAuthError.invalidCallback))
- return
- }
- completion(.success(callbackURL))
- }
- }
- extension OAuthAuthenticationManager: ASWebAuthenticationPresentationContextProviding {
- nonisolated func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
- MainActor.assumeIsolated {
- presentationWindow ?? NSApp.keyWindow ?? NSApp.mainWindow ?? NSApp.windows.first ?? ASPresentationAnchor()
- }
- }
- }
|