import AuthenticationServices import Foundation #if canImport(UIKit) import UIKit #else import AppKit #endif /// Ö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) } /// Das laufende Fenster — damit es sich auch von außen schließen lässt. /// /// Nötig, weil Plex den Rücksprung nicht immer ausführt: manchmal landet /// der Browser nach dem Anmelden auf watch.plex.tv und bleibt dort stehen. /// Der Token liegt dann längst bereit, nur das Fenster weiß es nicht. private var session: ASWebAuthenticationSession? /// Wie das Fenster ausgegangen ist. /// /// `nil`, solange es noch offen ist. private(set) var outcome: Failure? private(set) var didReturn = false /// Öffnet das Anmeldefenster und kehrt **sofort** zurück. /// /// Bewusst nicht abwartend: Plex führt den Rücksprung über `forwardUrl` /// nicht zuverlässig aus. Auf dem iPad landet der Browser nach dem /// Anmelden auf watch.plex.tv und bleibt stehen — der Token liegt längst /// bereit, aber ein abwartender Aufruf hinge auf einen Rückruf, der nie /// kommt. Der Aufrufer fragt deshalb parallel beim Server nach und sieht /// hier nur nach, ob das Fenster zwischendurch geschlossen wurde. func present(url: URL, callbackScheme: String) { outcome = nil didReturn = false let session = ASWebAuthenticationSession( url: url, callbackURLScheme: callbackScheme ) { [weak self] _, error in // ASWebAuthenticationSession ruft den Abschlussblock auf dem // Hauptthread auf — anders als VLCKit, wo dieselbe Annahme die App // zum Absturz gebracht hat. Hier ist sie dokumentiert. MainActor.assumeIsolated { guard let self else { return } if let error = error as? ASWebAuthenticationSessionError, error.code == .canceledLogin { self.outcome = .cancelled } else if let error { self.outcome = .presentationFailed(error.localizedDescription) } self.didReturn = true self.session = nil } } session.presentationContextProvider = self // `false`, damit eine bestehende Plex-Anmeldung in Safari mitgenutzt // wird: wer dort schon angemeldet ist, braucht nur zu bestätigen. session.prefersEphemeralWebBrowserSession = false guard session.start() else { outcome = .presentationFailed(String(localized: "signIn.windowFailed")) didReturn = true return } self.session = session } } extension PlexWebAuthenticator { /// Schließt das Anmeldefenster. /// /// Wird gerufen, sobald der Token auf anderem Weg eingetroffen ist — sonst /// bliebe der Browser mit der Plex-Seite offen stehen, obwohl die App /// schon angemeldet ist. func dismiss() { session?.cancel() session = nil } } extension PlexWebAuthenticator: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { // Das Fenster der App. Gesucht statt gehalten, weil eine SwiftUI-App // keinen festen Verweis darauf hat — und ein frisch erzeugtes Fenster // erschiene hinter dem sichtbaren. #if canImport(UIKit) let scene = UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .first { $0.activationState == .foregroundActive } return scene?.keyWindow ?? ASPresentationAnchor() #else return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor() #endif } }