import AuthenticationServices import Foundation import UIKit /// Öffnet die Anmeldeseite von Plex in einem Fenster innerhalb der App. /// /// `ASWebAuthenticationSession` ist dafür der vorgesehene Weg: die Seite kommt /// von Plex, das Passwort wird dort eingegeben und erreicht die App nie. Nach /// dem Anmelden schickt Plex den Browser auf eine Adresse mit unserem eigenen /// Schema, das Fenster schließt sich von selbst, und die App weiß, dass sie /// den Token abholen kann. /// /// Ein einfacher `SFSafariViewController` täte es nicht: er meldet nicht, wann /// die Rückadresse erreicht wurde, und müsste von Hand geschlossen werden. @MainActor final class PlexWebAuthenticator: NSObject { enum Failure: Error, Equatable { /// Der Nutzer hat das Fenster geschlossen. Kein Fehler im eigentlichen /// Sinn — die Oberfläche soll darauf kommentarlos zurückgehen. case cancelled case presentationFailed(String) } /// Führt die Anmeldung durch und kehrt zurück, sobald Plex die App /// zurückgeschickt hat. func authenticate(url: URL, callbackScheme: String) async throws(Failure) { do { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in let session = ASWebAuthenticationSession( url: url, callbackURLScheme: callbackScheme ) { _, error in if let error { continuation.resume(throwing: error) } else { continuation.resume() } } session.presentationContextProvider = self // `false`, damit eine bestehende Plex-Anmeldung in Safari // mitgenutzt wird: wer dort schon angemeldet ist, braucht nur // zu bestätigen statt das Passwort erneut einzugeben. session.prefersEphemeralWebBrowserSession = false guard session.start() else { continuation.resume(throwing: Failure.presentationFailed( String(localized: "signIn.windowFailed") )) return } } } catch let error as ASWebAuthenticationSessionError where error.code == .canceledLogin { throw .cancelled } catch let error as Failure { throw error } catch { throw .presentationFailed(error.localizedDescription) } } } extension PlexWebAuthenticator: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { // Das Fenster der App. Über die verbundenen Szenen gesucht, weil eine // SwiftUI-App keinen festen Verweis darauf hat — und ein frisch // erzeugtes Fenster erschiene hinter dem sichtbaren. let scene = UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .first { $0.activationState == .foregroundActive } return scene?.keyWindow ?? ASPresentationAnchor() } }