Anmeldung über die Plex-Seite statt über einen Code

Kein Code mehr zum Abtippen. Die Anmeldemaske kommt von Plex und öffnet
sich über ASWebAuthenticationSession in einem Fenster innerhalb der App;
nach dem Anmelden schickt Plex über forwardUrl auf ein eigenes Schema
zurück, das Fenster schließt sich von selbst.

Der PIN steckt weiterhin darunter — er ist der einzige Weg, auf dem Plex
einen Token herausgibt. Sichtbar ist er nicht mehr.

Was das bringt:
- Kein Fernseher-Gefühl mehr.
- Zwei-Faktor läuft mit, weil Plex die Seite selbst ausliefert.
- Das Passwort erreicht Flixy nie. Steht auch so unter dem Knopf.
- prefersEphemeralWebBrowserSession = false: wer in Safari schon bei
  Plex angemeldet ist, braucht nur zu bestätigen.

Zwei Details, die sonst still scheitern:
- Die Rückadresse wird strenger kodiert als die übrigen Werte. `:` und
  `/` lässt urlQueryAllowed durch, und für Plex endete der Wert dann
  beim ersten Schrägstrich — der Rücksprung käme nie.
- Das Schema muss in CFBundleURLTypes stehen, sonst kennt iOS es nicht
  und das Fenster bliebe offen stehen.

Als Schema die Bundle-Kennung statt eines kurzen Worts wie „flixy":
ein kurzes könnte eine andere App beanspruchen und die Anmeldung
abfangen.

132 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-09-11 20:22:52 +02:00
co-authored by Claude Opus 5
parent 70c620bad4
commit 1756ed11cc
10 changed files with 2281 additions and 2123 deletions
+2 -4
View File
@@ -24,12 +24,10 @@ struct RootView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.flixyCanvas)
case .signedOut, .awaitingAuthorization:
case .signedOut, .authenticating:
SignInView(
phase: session.phase,
clientIdentifier: session.clientIdentifier,
onSignIn: { Task { await session.beginSignIn() } },
onCancel: { session.cancelSignIn() }
onSignIn: { Task { await session.beginSignIn() } }
)
case .choosingServer(let servers):
+24 -60
View File
@@ -4,18 +4,20 @@ import PlexKit
/// Der Anmeldebildschirm.
///
/// Zeigt den Code, den Plex vergeben hat, und schickt in den Browser. Die App
/// bekommt Benutzername und Passwort nie zu sehen — sie fragt nur regelmäßig
/// nach, ob der Code bestätigt wurde.
/// Ein Knopf, mehr nicht. Die Anmeldemaske kommt von Plex und öffnet sich in
/// einem Fenster innerhalb der App; das Passwort wird dort eingegeben und
/// erreicht Flixy nie. Nach dem Anmelden schließt sich das Fenster von selbst.
struct SignInView: View {
let phase: FlixySession.Phase
let clientIdentifier: String
let onSignIn: () -> Void
let onCancel: () -> Void
@Environment(\.openURL) private var openURL
@Environment(\.flixyAccent) private var accent
private var isAuthenticating: Bool {
if case .authenticating = phase { return true }
return false
}
var body: some View {
VStack(spacing: FlixySpacing.section) {
Spacer()
@@ -37,17 +39,26 @@ struct SignInView: View {
Spacer()
if case .awaitingAuthorization(let pin) = phase {
awaitingAuthorization(pin: pin)
} else {
VStack(spacing: FlixySpacing.regular) {
Button(action: onSignIn) {
Text("signIn.action")
.font(.flixyRowHeader)
.frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.regular)
Group {
if isAuthenticating {
ProgressView().tint(Color.flixyCanvas)
} else {
Text("signIn.action").font(.flixyRowHeader)
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.regular)
}
.buttonStyle(.borderedProminent)
.tint(accent)
.disabled(isAuthenticating)
Text("signIn.privacy")
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextMuted)
.multilineTextAlignment(.center)
}
Spacer().frame(height: FlixySpacing.section)
@@ -57,51 +68,4 @@ struct SignInView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.flixyCanvas)
}
@ViewBuilder
private func awaitingAuthorization(pin: PlexPin) -> some View {
VStack(spacing: FlixySpacing.loose) {
Text("signIn.codeHint")
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextSecondary)
.multilineTextAlignment(.center)
Text(pin.code)
// Feste Zeichenbreite, damit der Code beim Abtippen nicht
// springt und Ziffern nicht mit Buchstaben verschwimmen.
.font(.system(.largeTitle, design: .monospaced, weight: .semibold))
.tracking(6)
.foregroundStyle(Color.flixyTextPrimary)
.textSelection(.enabled)
.padding(.vertical, FlixySpacing.regular)
.frame(maxWidth: .infinity)
.background(Color.flixyElevated, in: .rect(cornerRadius: FlixyRadius.sheet))
Button {
if let url = pin.authorizationURL(
clientIdentifier: clientIdentifier,
product: Branding.plexProduct
) {
openURL(url)
}
} label: {
Label("signIn.openBrowser", systemImage: "safari")
.frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.regular)
}
.buttonStyle(.borderedProminent)
.tint(accent)
HStack(spacing: FlixySpacing.snug) {
ProgressView().controlSize(.small)
Text("signIn.waiting")
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextMuted)
}
Button("action.cancel", action: onCancel)
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextSecondary)
}
}
}