Shuffle/docs/specs/49-mobil-anmeldung-app-links.md
cschlaefke 34bba3439b docs(auth): address review findings on spec 49
Existing team KSX6GJY9C5 (C1), defer CODE_SIGN_ENTITLEMENTS until
the team is confirmed, local mobile builds mandatory, email_change
needs KTO-11, verified facts separated from spike assumptions,
QUE-06 keyboard criterion, T2 depends on PR A, limited interception
protection on Android < 12 (R13, F11), login CSRF accepted (R14),
supersession of spec 1 E4 recorded as task W2.

Refs #49

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 20:00:45 +00:00

48 KiB
Raw Blame History

Spec #49 – Mobil: Einstieg in die Anmeldung und Rückweg aus E-Mail-Links (iOS/Android)

Issue #49 (type/tech, prio/should, Milestone R1), Refs #1
Status Entwurf, wartet auf Freigabe durch Chris
Autor architect
Stand 2026-11-03
Abhängigkeit KTO-01 (#1) umgesetzt; berührt QUE-09 (#29) Kriterium 13 und KTO-11 (#28); Betrieb nach ADR-0002 (Aufgabe D1 aus Spec #1, noch ohne eigenes Issue)
Quellen Issue #49, docs/specs/1-auth-registrierung-anmeldung.md (E3, E4, § 5.3, § 5.4, § 6), docs/auth-konfiguration.md (AUTH-CFG-01, -02, § 3), app/lib/core/routing/, app/lib/features/auth/presentation/providers/auth_redirect.dart, requirements/20-r1-akzeptanzkriterien.md § QUE-09, § KTO-11, requirements/r1-klaerungen.md N9, ADR-0001, ADR-0002, Quelltext supabase_flutter 2.x (SupabaseAuth) und app_links 7.2.2 (iOS-Plugin)

1. Ziel

Auf iOS und Android sollen Nutzer (a) aus der App heraus zur Anmeldung und Registrierung kommen und (b) mit den Links aus der Bestätigungs- und der Reset-Mail in der App landen, wenn sie installiert ist. Ohne App und auf anderen Geräten bleibt die Web-App der Zielort; der geräteübergreifende Ablauf aus Spec #1 E3 bleibt erhalten. Spec #1 E4 („Mail-Links landen in R1 immer in der Web-App“) wird durch diese Spec abgelöst (E2; Verweis in Spec #1 über Aufgabe W2). Das Verhalten im Web ändert sich bei den Mail-Links nicht.

Abweichung vom Vorschlag im Issue: Das Issue schlägt Universal/App Links für /auth/confirm und /reset-password vor. Diese Spec beansprucht nur /auth/confirm (E2), weil /reset-password nie Ziel eines Mail-Links ist.

Befund (Issue #49, iOS-Simulator, main @ 455b3bf):

  1. Die öffentliche Startseite (HomeScreen) hat keinen Weg zu /login oder /register. privateRoutePaths ist leer, deshalb leitet auch kein Guard dorthin. Auf Mobilgeräten sind die Auth-Screens dadurch nicht erreichbar.
  2. Die Mail-Links zeigen auf <SITE_URL>/auth/confirm?token_hash=…&type=email|recovery. Die App beansprucht diese URLs nicht, deshalb öffnet der Link den Browser. Lokal zeigt SITE_URL auf http://localhost:3000, wo im Simulator keine Web-App läuft; dort lässt sich die Bestätigung gar nicht abschließen. In Produktion würde der Link in der mobilen Web-App funktionieren, der Nutzer wäre dann aber im Browser angemeldet und nicht in der App.

1.1 Optionen

Einstieg in die Anmeldung

Option Beschreibung Vorteile Nachteile
E-a Hinweis-Karte auf der Startseite (abgemeldet): kurzer Text, „Anmelden“, „Konto erstellen“ Entspricht QUE-09 Kriterium 13 („Hinweis mit dem Weg zum Anmelden und Registrieren“, N9); QUE-09 kann die Karte übernehmen; klein Nur auf der Startseite sichtbar (bisher die einzige öffentliche Seite)
E-b Aktion „Anmelden“ in der AppBar Von jeder Seite mit AppBar erreichbar Es gibt noch keine gemeinsame AppBar bzw. Shell; das wäre eine Shell-Entscheidung vor QUE-09/KTO-11; schwächeres Touch-Ziel und Text-Platz bei 200 % Schrift
E-c Navigationsleiste mit Tab „Konto“ Späterer Ort für KTO-11 Vorgriff auf Navigation und Einstellungen ohne Inhalt; größerer Eingriff
E-d Private Startseite (Guard leitet abgemeldet nach /login) Kein UI nötig Widerspricht QUE-09 Kriterium 13 und Sicherheit Kriterium 2 (Katalog öffentlich), Web-Startseite wäre nicht mehr öffentlich

Rückweg aus den Mail-Links

Option Beschreibung Vorteile Nachteile
L-a Universal Links (iOS) und App Links (Android) auf der bestehenden https-Adresse <SITE_URL>/auth/confirm Ein Link für alle Geräte; ohne App öffnet derselbe Link die Web-App (Fallback ohne Zusatzaufwand); Mailvorlagen, GoTrue und Web bleiben unverändert; kein Fremdanbieter; Links sind an Domain, Team-ID und Signatur gebunden; eine fremde App kann sie nach erfolgreicher Verifizierung nicht abfangen (Android: verlässlich erst ab Android 12, siehe R13) Braucht Apple-Developer-Konto (Team-ID), Play-Signaturschlüssel, endgültige Domain und IDs sowie öffentlich erreichbare Zuordnungsdateien; lokal kaum Ende-zu-Ende testbar; manche Mail-Programme lösen Universal Links voraussichtlich nicht aus (z. B. Outlook Safe Links, In-App-Browser)
L-b Eigenes URL-Schema (shuffle://auth/confirm?…) direkt in der Mail Lokal sofort testbar, keine Konten nötig Abgelehnt. Ohne App öffnet der Link nichts, das widerspricht dem Fallback im Web (Kriterium 4) und dem geräteübergreifenden Ablauf (E3, Registrierung am Handy, Mail am PC); viele Mail-Programme machen Nicht-http-Links nicht klickbar; jede App kann dasselbe Schema registrieren und den token_hash abfangen, beim Reset-Link wäre das eine Kontoübernahme
L-c Eigenes Schema als redirect_to über {{ .ConfirmationURL }} (GoTrue /verify leitet zur App weiter) Supabase-Standardweg Abgelehnt. Hebt E3 auf: Der Link würde beim ersten Aufruf eingelöst, Mail-Scanner verbrauchen ihn; geräteübergreifend nur mit PKCE-Verifier; Abfang-Risiko wie L-b; Allow-List müsste das Schema aufnehmen
L-d Code statt Link (6-stelliges OTP {{ .Token }} in der Mail, Eingabe in der App) Keine Domain-Dateien, lokal testbar Widerspricht KTO-01 Kriterium 8 („Link“); neue UX und neue Mailvorlagen; 6 Ziffern sind ohne Drosselung je Konto ratbar (Drosselung nur je IP, AUTH-CFG-12)
L-e Status quo: Link öffnet immer die Web-App, danach in der App anmelden Kein Aufwand, funktioniert in Produktion Erfüllt Kriterien 2 und 3 nicht; lokal im Simulator nicht abschließbar

1.2 Empfehlung und Kernentscheidungen

# Entscheidung (Vorschlag) Begründung
E1 Einstieg über eine Hinweis-Karte auf der Startseite (E-a), auf allen Plattformen inklusive Web, nur im Status signedOut. Die Karte ist ein eigenes Widget SignInPrompt in features/auth/presentation/widgets/, das QUE-09 später auf der neuen Startseite wiederverwendet. Keine AppBar-Aktion, keine Navigationsleiste. Deckt Kriterium 1 und vorab den Anmelde-Teil von QUE-09 Kriterium 13 ab. Eine Shell oder Navigationsleiste ist eine eigene Entscheidung mit QUE-09/KTO-11. Gleiches Verhalten auf allen Plattformen ist einfacher als eine Weiche nur für Mobile.
E2 Universal Links und App Links (L-a), beschränkt auf genau den Pfad /auth/confirm. /reset-password gehört nicht dazu: Es ist nie Ziel eines Mail-Links, die App erreicht es per Redirect nach verifyOTP (Spec #1 § 5.3). Alle anderen Web-Adressen öffnen weiter den Browser. Kleinster nötiger Umfang. Spätere öffentliche Web-Adressen (Katalog, Spieldetail) sollen nicht ungeplant in der App landen. Eine Erweiterung ist eine eigene Entscheidung (F1). KTO-11 (E-Mail-Wechsel, type=email_change) nutzt denselben Pfad und profitiert bei der Link-Zuordnung ohne Änderung. EmailLinkType.tryParse (app/lib/features/auth/domain/email_link_type.dart) kennt heute nur email, signup und recovery; email_change ergäbe „ungültig“. EmailLinkType und die Mailvorlage ergänzt KTO-11 (#28), Hinweis dort.
E3 Kein eigenes URL-Schema, weder in Mails noch als Fallback (L-b, L-c abgelehnt). Sicherheit (Abfangen des Reset-Tokens), Fallback ohne App, E3 aus Spec #1.
E4 Keine Backend-Änderung. Keine Migration, keine Edge Function, Mailvorlagen und GOTRUE_URI_ALLOW_LIST (AUTH-CFG-02) bleiben unverändert. Einzige Bedingung: Der Host von SITE_URL (AUTH-CFG-01) ist genau die Domain, die die Zuordnungsdateien ausliefert und in den App-Projekten eingetragen ist. Die Vorlagen bauen den Link aus {{ .SiteURL }} und {{ .TokenHash }}, nicht aus GoTrue /verify?redirect_to=…. Die Allow-List greift dabei nicht.
E5 Deep Links über das eingebaute Flutter-Deep-Linking an go_router, kein zusätzliches Paket. FlutterDeepLinkingEnabled (iOS) und flutter_deeplinking_enabled (Android) werden ausdrücklich auf true gesetzt. EmailLinkScreen bleibt unverändert der Zielort, auch der Knopfdruck vor verifyOTP (Spec #1 E3) bleibt. go_router verarbeitet Pfad und Query bereits (Route /auth/confirm existiert). Belegt im Quelltext (Review, 2026-11-03): supabase_flutter 2.18.3 hört über app_links (transitiv vorhanden) mit, reagiert aber nur auf access_token, code, error* (SupabaseAuth._defaultIsAuthCallbackDeeplink) und ignoriert unsere Links mit token_hash; das iOS-Plugin von app_links 7.2.2 gibt Links standardmäßig an andere Empfänger weiter (defaultUrlHandling = .never); die Flutter-Engine 3.47.9 verarbeitet Deep Links selbst (iOS: FlutterDeepLinkingEnabled ohne Eintrag YES; Android: onNewIntent pusht die Route); go_router loggt Routen nur mit debugLogDiagnostics. Das Zusammenspiel am Gerät bzw. Simulator (UIScene, Kalt- und Warmstart) prüft S1.
E6 Ein Issue, zwei PRs: PR A „Einstieg“ (sofort umsetzbar, Kriterium 1), PR B „App Links“ (Kriterien 2–4). PR B ist für Android und die Vorlagen ohne Chris-Aufgaben mergebar (Dateien mit Platzhaltern, geprüft durch Tests). Einschränkung iOS: Das Xcode-Projekt nutzt bereits DEVELOPMENT_TEAM = KSX6GJY9C5. Ob das ein bezahltes Team ist, klärt C1. Bis dahin legt PR B Runner.entitlements an, verdrahtet CODE_SIGN_ENTITLEMENTS aber noch nicht (oder nur über eine xcconfig-Weiche, die standardmäßig aus ist), damit Gerätebuilds mit einem kostenlosen Personal Team nicht brechen. Die CI baut nur Web und würde den Bruch nicht bemerken. Die Gerätebestätigung der Kriterien 2 und 3 hängt an C1–C6 (§ 8.3). PR A soll nicht auf Konten und Domain warten; ein iOS-Gerätebuild darf nicht unbemerkt brechen.
E7 Zuordnungsdateien liegen als Vorlage im Repo (Team-ID und Zertifikat-Fingerabdrücke sind nicht geheim); ein Test prüft sie gegen Bundle-ID, Application-ID, Pfad und Host. Wo sie ausgeliefert werden (Web-Build oder Gateway), klärt S1. Repo und Betrieb bleiben deckungsgleich (ADR-0002: Einstellungen im Repo festhalten).
E8 Host je Plattform an genau einer Stelle konfiguriert (Android: Gradle-Property, iOS: xcconfig-Variable), Standard ist die Produktions-Domain (Platzhalter shuffle.cshomelabs.work, F8 aus Spec #1). Domain ist noch Platzhalter; eine Änderung soll eine Zeile je Plattform sein. Staging-Builds sind offen (F5).

2. Akzeptanzkriterien

Übernommen aus Issue #49 (Entwurf). Referenz in Tests und PRs: „#49 Kriterium n“.

  1. Auf iOS und Android ist die Anmeldung aus der App heraus erreichbar.
  2. Der Bestätigungslink aus der Mail öffnet die App und schließt die Registrierung ab.
  3. Der Passwort-Reset-Link öffnet die App und führt in den Reset-Ablauf.
  4. Verhalten ohne installierte App (Fallback im Web) ist beschrieben.

Vorschläge zur Präzisierung (nicht entschieden, Freigabe durch Chris, siehe F10):

  • Zu 1: Bin ich abgemeldet, zeigt die Startseite einen Hinweis mit „Anmelden“ und „Konto erstellen“, die zu /login bzw. /register führen. Das gilt auch im Web. Bin ich angemeldet, erscheint der Hinweis nicht.
  • Zu 2: Gegeben die App ist installiert und ich tippe auf dem Gerät auf den Bestätigungslink, dann öffnet sich die Bestätigungsseite in der App; nach dem Knopfdruck „E-Mail-Adresse bestätigen“ bin ich in der App angemeldet. Das gilt bei kaltem Start und bei laufender App.
  • Zu 3: wie 2 für den Reset-Link; nach dem Knopfdruck führt die App zu „Neues Passwort“, danach gelten KTO-01 Kriterien 8 und 9 unverändert.
  • Zu 4 (testbar formuliert): Gegeben die App ist nicht installiert, ich öffne den Link an einem anderen Gerät, oder das Mail-Programm übergibt den Link nicht an die App, dann öffnet sich die Web-App und schließt Bestätigung bzw. Reset ab wie bisher (Spec #1 § 5.4); nach einer Bestätigung im mobilen Browser sehe ich den bestehenden Hinweis „Du kannst dich jetzt auch in der App anmelden.“
  • Zusätzlich 5: Im Web ändert sich das Verhalten der Mail-Links nicht (die bestehenden Playwright-Tests web/* bleiben unverändert grün).

Relevante Querschnittskriterien: QUE-05 Kriterien 2 und 4 (neue Texte in ARB, de/en); QUE-06 Kriterien 1–4 (Karte: Beschriftung, Kontrast AA, Touch-Ziele ≥ 48 dp, 200 % Schrift, Tastatur im Web); Sicherheit der Daten Kriterium 2 (Startseite bleibt öffentlich, keine privaten Daten); Plattformen Kriterien 1 und 2.

3. Datenmodell

Keine Änderung. Keine neuen Tabellen, Spalten, Policies oder pgTAP-Tests.

RLS: unverändert. Der Deep Link überträgt nur token_hash und type; eingelöst wird er wie bisher per verifyOTP gegen GoTrue.

4. Backend

4.1 Supabase / GoTrue

Punkt Wert Änderung
Mailvorlagen (AUTH-CFG-17, -18) Link {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email bzw. …&type=recovery keine
GOTRUE_SITE_URL (AUTH-CFG-01) Prod: https://<Produktions-Domain> Bedingung: Host = Associated Domain (APPLINK-01). Auf Prod setzt das Chris (C5).
GOTRUE_URI_ALLOW_LIST (AUTH-CFG-02) <SITE_URL>/auth/confirm keine (kein redirect_to im Einsatz)
lokal config.toml site_url = "http://localhost:3000" keine; lokal gibt es keine Universal Links (siehe 6.2)
RPCs / Edge Functions – keine

4.2 Neue Konfigurations-IDs (Abschnitt in docs/auth-konfiguration.md)

Neuer Abschnitt „App Links / Universal Links“ mit eigenen IDs, damit Tests und Release-Checkliste darauf verweisen. Die bestehenden AUTH-CFG-IDs bleiben unverändert.

ID Einstellung Wert Ort
APPLINK-01 Associated Domain Host von SITE_URL (Prod), Platzhalter shuffle.cshomelabs.work Android: Gradle-Property shuffleAppLinkHost (Standard in android/gradle.properties), über manifestPlaceholders["appLinkHost"]; iOS: APP_LINK_HOST in ios/Flutter/AppLinks.xcconfig, eingebunden in Debug.xcconfig und Release.xcconfig
APPLINK-02 Beanspruchter Pfad genau /auth/confirm (mit beliebiger Query) Android-Intent-Filter, AASA components, assetlinks (Android verifiziert je Host, der Pfad steht im Intent-Filter)
APPLINK-03 iOS-App-ID <TEAM_ID>.work.cshomelabs.shuffle (Bundle-ID Platzhalter, R15 aus Spec #1). Im Xcode-Projekt steht bereits DEVELOPMENT_TEAM = KSX6GJY9C5; bestätigt Chris es als bezahltes Team (C1), wird <TEAM_ID> durch KSX6GJY9C5 ersetzt apple-app-site-association
APPLINK-04 Android-Paket und Signatur work.cshomelabs.shuffle, SHA-256 des Upload-Schlüssels und des Play-App-Signing-Schlüssels assetlinks.json
APPLINK-05 Auslieferung https://<Domain>/.well-known/apple-app-site-association und /.well-known/assetlinks.json: HTTPS mit gültigem Zertifikat, Status 200 ohne Weiterleitung, Content-Type: application/json, öffentlich erreichbar (Apple lädt über sein CDN), vom SPA-Rewrite ausgenommen Gateway bzw. Web-Hosting (D1, C5)
APPLINK-06 Flutter-Deep-Linking FlutterDeepLinkingEnabled = true (Info.plist), flutter_deeplinking_enabled = true (Manifest) App-Projekte

4.3 Zuordnungsdateien (Vorlagen)

Ablageort laut S1: bevorzugt app/web/.well-known/ (wenn flutter build web versteckte Verzeichnisse mitkopiert und das Hosting sie korrekt ausliefert), sonst deploy/well-known/ mit Auslieferung am Gateway. Platzhalter werden im Repo durch die echten Werte ersetzt, sobald C1–C3 erledigt sind.

apple-app-site-association (ohne Dateiendung):

{
  "applinks": {
    "details": [
      {
        "appIDs": ["<TEAM_ID>.work.cshomelabs.shuffle"],
        "components": [
          { "/": "/auth/confirm", "comment": "E-Mail-Links (Bestätigung, Reset, später E-Mail-Wechsel)" }
        ]
      }
    ]
  },
  "webcredentials": {
    "apps": ["<TEAM_ID>.work.cshomelabs.shuffle"]
  }
}

assetlinks.json:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "work.cshomelabs.shuffle",
      "sha256_cert_fingerprints": ["<UPLOAD_KEY_SHA256>", "<PLAY_APP_SIGNING_SHA256>"]
    }
  }
]

webcredentials (iOS) und delegate_permission/common.get_login_creds (Android, optional) verbinden die Passwortspeicher von App und Web. Ob sie aufgenommen werden, entscheidet F2 (Empfehlung: webcredentials ja).

4.4 Fehlerfälle

Keine neuen Server-Fehler. Alle Fehler beim Einlösen laufen wie bisher über das Mapping aus Spec #1 § 4.4 (linkInvalidOrExpired, network, rateLimited, unexpected). Ein Link einer anderen Umgebung (z. B. Staging-Link in der Prod-App) ergibt beim Einlösen linkInvalidOrExpired; das ist harmlos.

5. Frontend

5.1 PR A: Einstieg (Kriterium 1)

Neues Widget SignInPrompt (app/lib/features/auth/presentation/widgets/sign_in_prompt.dart):

  • Inhalt: kurze Überschrift und ein Satz (Vorschlag ui-designer, z. B. „Melde dich an, um deine Sammlung und Partien zu verwalten.“), Primärknopf „Anmelden“ → context.go(AppRoutes.login), Sekundärknopf „Konto erstellen“ → context.go(AppRoutes.register). Kein from-Parameter (Ziel nach dem Login ist ohnehin /).
  • Komponenten aus core/widgets (AppPrimaryButton, für den Sekundärknopf gegebenenfalls eine neue Variante durch ui-designer). Touch-Ziele ≥ 48 dp, Semantics über die Knopf-Labels, Überschrift als Semantics(header: true).
  • Dumm: bekommt keine Logik, nur Navigation per Callback oder context.go.

HomeScreen wird ConsumerWidget:

  • Liest nur showSignInPromptProvider (siehe unten), nicht authStatusProvider direkt. Der Provider bildet die Zustände so ab:
    • signedOut → Platzhaltertext und darunter SignInPrompt.
    • signedIn → Platzhaltertext wie bisher, kein Prompt (keine Abmelden-Funktion, siehe F4).
    • passwordRecovery → tritt auf / nicht auf (Redirect nach /reset-password).
    • Laden (Stream ohne ersten Wert) → Platzhaltertext ohne Prompt, damit beim Start mit gespeicherter Sitzung der Prompt nicht kurz aufblinkt. Der Wert kommt praktisch sofort (initialSession).
    • Fehler im Stream → wie signedOut behandeln (Prompt anzeigen), Fehler nur ins Log.
  • Die Entscheidung „Prompt ja/nein“ trifft ein kleiner Provider showSignInPromptProvider (bool) in features/auth/presentation/providers/, der authStatusProvider liest; das Widget entscheidet nichts. Damit bleibt der HomeScreen ohne Logik und QUE-09 übernimmt Provider und Widget.
  • Import über Feature-Grenzen (home → auth/presentation): bewusst. Der Anmelde-Hinweis gehört fachlich zu auth (Routen, Status); home ist nur Gastgeber. Die Abhängigkeit geht in eine Richtung (auth kennt home nicht), und QUE-09 ersetzt home später durch die Startseite, die denselben Import nutzt. Ein Verschieben nach core/widgets lohnt erst mit einem zweiten Gastgeber.
  • Breite Bildschirme: Karte max. 480 dp breit, zentriert (wie AppFormScaffold).

Texte (ARB, de/en): signInPromptTitle, signInPromptBody, signInPromptSignIn, signInPromptRegister. Bestehende Schlüssel für „Anmelden“/„Konto erstellen“ dürfen wiederverwendet werden, wenn der Wortlaut passt.

Keine neue Route, kein neuer Screen. Der Deep Link landet auf der bestehenden Route /auth/confirm (EmailLinkScreen mit token_hash und type).

Abläufe in der App:

Situation Verhalten
Kaltstart über den Link Flutter übergibt /auth/confirm?token_hash=…&type=… als Startroute; erwartet: go_router nutzt sie statt initialLocation (zu bestätigen in S1 (a); sonst overridePlatformDefaultLocation prüfen). Redirect-Regeln wie bisher: signedOut/signedIn → bleibt; passwordRecovery → /reset-password.
App läuft (warm) Flutter meldet die neue Route (Android: onNewIntent pusht sie, belegt); erwartet: go_router ersetzt den Stack (go), zu bestätigen in S1 (f). Nicht gespeicherte Eingaben auf dem vorherigen Screen gehen dann verloren (R10).
Bestätigung erfolgreich Hinweis „E-Mail bestätigt“, Knopf „Weiter“ → /. Nutzer ist in der App angemeldet, die Startseite zeigt keinen Prompt mehr. Der Zusatz „auch in der App anmelden“ erscheint in der App nicht (isMobileWebProvider ist dort false).
Reset erfolgreich Status passwordRecovery → /reset-password (Spec #1 § 5.4, 5.5 unverändert, Guard nur im Prozess).
Link ungültig/abgelaufen Fehlerzustand mit „Neuen Link anfordern“ wie bisher.
Zurück-Taste (Android) auf /auth/confirm nach Kaltstart Kein Eintrag darunter: Die App schließt sich. Akzeptiert; „Weiter“ führt zur Startseite.
Link öffnet, während ein anderes Konto angemeldet ist verifyOTP ersetzt die Sitzung, wie heute im Web (F3).

Plattform-Dateien (Umsetzung flutter-dev):

  • android/app/src/main/AndroidManifest.xml: in MainActivity zusätzlicher Intent-Filter mit android:autoVerify="true", action VIEW, category DEFAULT und BROWSABLE, data android:scheme="https" android:host="${appLinkHost}" android:path="/auth/confirm". Dazu meta-data flutter_deeplinking_enabled = true. launchMode="singleTop" bleibt (warme Links kommen über onNewIntent).
  • android/app/build.gradle.kts: manifestPlaceholders["appLinkHost"] aus der Gradle-Property shuffleAppLinkHost (APPLINK-01).
  • ios/Runner/Runner.entitlements (neu): com.apple.developer.associated-domains = applinks:$(APP_LINK_HOST) und (F2) webcredentials:$(APP_LINK_HOST). Verdrahtung (E6): CODE_SIGN_ENTITLEMENTS wird erst gesetzt, wenn C1 bestätigt, dass KSX6GJY9C5 ein bezahltes Team ist. Bis dahin bleibt die Einstellung leer, oder sie kommt über eine xcconfig-Variable (z. B. SHUFFLE_ENTITLEMENTS in ios/Flutter/AppLinks.xcconfig, Standard leer), die nach C1 auf Runner/Runner.entitlements gesetzt wird. Für Tests auf Geräten im Entwicklermodus optional applinks:$(APP_LINK_HOST)?mode=developer (umgeht den Apple-CDN-Cache), nur in Debug. Mechanismus: der Suffix kommt aus einer Variablen APP_LINK_MODE_SUFFIX, die nur Debug.xcconfig auf ?mode=developer setzt (Release.xcconfig lässt sie leer); der Eintrag lautet dann applinks:$(APP_LINK_HOST)$(APP_LINK_MODE_SUFFIX). Alternative: eigene Runner-Debug.entitlements nur für Debug.
  • ios/Runner/Info.plist: FlutterDeepLinkingEnabled = true.
  • ios/Runner/SceneDelegate.swift: unverändert, sofern S1 zeigt, dass FlutterSceneDelegate Universal Links (scene(_:willConnectTo:options:) und scene(_:continue:)) an die Engine weitergibt.

Logging: token_hash darf in keinem Log erscheinen (Spec #1: keine personenbezogenen bzw. geheimen Daten). Zu prüfen: go_router debugLogDiagnostics bleibt aus, AppLogger und der globale Fehler-Handler loggen keine vollständigen Routen-URIs, EmailLinkController loggt nur Typ und Ergebnis. supabase_flutter loggt den URI nur für Links, die es selbst verarbeitet (nicht unsere).

5.3 Domänenmodelle, Provider

  • Neu: showSignInPromptProvider (bool aus authStatusProvider, Regeln aus 5.1).
  • Unverändert: AuthRepository, AuthStatus, EmailLinkController, Redirect-Regeln (authRedirectFor), privateRoutePaths (bleibt leer).

5.4 Neue Pakete

Keine. app_links ist über supabase_flutter bereits transitiv vorhanden (7.2.2, Apache-2.0, 160/160 Punkte, pub.dev geprüft 2026-11-03) und wird nicht direkt genutzt. Rückfall, falls S1 zeigt, dass Flutters eingebautes Deep-Linking mit dem app_links-Plugin kollidiert: FlutterDeepLinkingEnabled=false setzen, app_links als direkte Abhängigkeit aufnehmen und dessen uriLinkStream in einem kleinen Adapter in core/routing an router.go() weitergeben. Diese Variante bräuchte eine kurze Ergänzung der Spec.

6. Plattform-Besonderheiten

6.1 Übersicht

Thema iOS Android Web
Einstieg (Krit. 1) Hinweis-Karte auf / Hinweis-Karte auf / Hinweis-Karte auf / (auch hier, QUE-09 Krit. 13); URLs /login, /register weiter direkt aufrufbar
Link-Zuordnung Universal Links: Entitlement applinks:<Host>, AASA wird bei Installation und regelmäßig über Apples CDN geladen App Links: Intent-Filter mit autoVerify, assetlinks.json wird bei Installation geprüft (Android 12+: ohne erfolgreiche Prüfung öffnet der Browser) keine Änderung; /auth/confirm wie bisher (SPA-Rewrite, D1)
Voraussetzungen (Konten) Bezahltes Apple-Developer-Programm (Team-ID, Capability „Associated Domains“; mit einem kostenlosen Personal Team nach unserem Kenntnisstand nicht möglich, C1 bestätigt) Play Console und Play App Signing (Fingerabdruck), eigener Upload-Schlüssel statt Debug-Signatur (heute signiert release mit dem Debug-Schlüssel) –
Wann öffnet die App nicht? Link in Safari-Adresszeile eingegeben; Link über Weiterleitungsdienste (voraussichtlich Outlook Safe Links, Tracking-Redirects); manche In-App-Browser von Mail-Apps; Nutzer hat „In Safari öffnen“ gewählt (iOS merkt sich das je Domain) App-Link-Prüfung fehlgeschlagen; Nutzer hat in den Einstellungen „Unterstützte Links öffnen“ deaktiviert; manche In-App-Browser –
Android-Versionen – minSdk = flutter.minSdkVersion, also auch Android 7–11. Dort zeigt Android bei fehlender oder fehlgeschlagener Verifizierung einen Auswahldialog; eine andere App mit passendem Intent-Filter kann den Link dann empfangen. Verlässlicher Abfangschutz erst ab Android 12 mit erfolgreicher Verifizierung (R13) –
Fallback Web-App öffnet /auth/confirm und schließt den Schritt ab; der Nutzer ist dann im Browser angemeldet, nicht in der App; Hinweis „auch in der App anmelden“ (bestehend) gleich ist der Normalfall
Debug-Builds Kaltstart einer Debug-Build per Link funktioniert nicht ohne Flutter-Tooling (bekannte Flutter-Einschränkung; Quelle: Hinweis im iOS-Plugin von app_links 7.2.2 mit Verweis auf flutter/flutter#149214 und llfbandit/app_links#211); Gerätetests mit Profile/Release Debug funktioniert, solange der Debug-Fingerabdruck in assetlinks.json steht (nur Staging, F5) –
Kamera, Dateien, Push, Offline nicht betroffen; Einlösen braucht Netz (Offline-Hinweis aus Spec #1) gleich gleich

6.2 Lokale Entwicklung

  • Lokal (site_url = http://localhost:3000) gibt es keine Universal/App Links. Die Mails in Mailpit zeigen weiter auf localhost:3000, dort funktioniert der Web-Ablauf.
  • Android lokal prüfbar: Den token_hash aus der Mailpit-Mail nehmen und mit dem App-Link-Host aufrufen: adb shell am start -W -a android.intent.action.VIEW -d "https://<APP_LINK_HOST>/auth/confirm?token_hash=<…>&type=email" work.cshomelabs.shuffle. Mit expliziter Paketangabe startet die App auch ohne erfolgreiche Verifizierung; sie löst den Token gegen das lokale Supabase ein (Env dev.json). Verifizierungsstatus: adb shell pm get-app-links work.cshomelabs.shuffle.
  • iOS lokal: Ohne Team-ID und erreichbare AASA nicht Ende-zu-Ende prüfbar. Die App-Logik deckt ein Widget-Test mit simulierter Plattform-Route ab (7.1). Ende-zu-Ende erst auf Staging/Prod mit echter Domain (C6).

7. Teststrategie

Werkzeuge: flutter_test + mocktail (Unit/Widget), bestehende Playwright-Tests (e2e/, Projekt web) als Regression, ein Konfigurations-Test für die Plattform-Dateien, manuelle Gerätematrix.

7.1 Abdeckungsmatrix

AK Unit Widget Integration/E2E RLS Task
#49 Krit. 1 showSignInPromptProvider: signedOut → true, signedIn → false, Laden → false, Fehler → true HomeScreen abgemeldet: Prompt mit zwei Knöpfen sichtbar; Tipp „Anmelden“ → LoginScreen, „Konto erstellen“ → RegisterScreen; angemeldet: kein Prompt; a11y-Guidelines (labeledTapTargetGuideline, androidTapTargetGuideline, iOSTapTargetGuideline, textContrastGuideline), textScaler 2.0 ohne Overflow, de/en Playwright web: Startseite abgemeldet zeigt den Prompt, Klick führt zu /login und /register (Web gehört dazu, E1); QUE-06 Kriterium 4: per Tab erreichbar in Lesereihenfolge (Überschrift/Text → „Anmelden“ → „Konto erstellen“), sichtbarer Fokus, Enter bzw. Leertaste löst den fokussierten Knopf aus – U1, A1, T1
#49 Krit. 2 – Kaltstart: tester.binding.platformDispatcher.defaultRouteNameTestValue = '/auth/confirm?token_hash=x&type=email' → EmailLinkScreen mit Typ Bestätigung; warm: tester.binding.handlePushRoute(...) bzw. SystemChannels.navigation pushRouteInformation → EmailLinkScreen; Knopfdruck ruft verifyEmailLink (Mock) genau einmal; Erfolg → „Weiter“ → /; die Prüfung „Prompt verschwunden“ setzt PR A voraus (T2 hängt deshalb von A1 ab, § 8.2) manuell M1–M4 (7.2) – A2, T2
#49 Krit. 3 – wie Krit. 2 mit type=recovery: nach Erfolg und Status passwordRecovery → ResetPasswordScreen; Deep Link während passwordRecovery → bleibt auf /reset-password manuell M5–M6 – A2, T2
#49 Krit. 4 – – Playwright web (bestehend, unverändert): kompletter Bestätigungs- und Reset-Ablauf im Browser; manuell M7–M9 (App nicht installiert, PC, Outlook) – T2
Vorschlag 5 (Web unverändert) bestehende Redirect-Tests (authRedirectFor) unverändert grün – alle Playwright-Projekte api, web, throttle unverändert grün – T2
APPLINK-01 bis -06 Konfigurations-Test app/test/platform/app_links_config_test.dart (liest Dateien relativ zu app/): Manifest enthält Intent-Filter mit autoVerify="true", scheme=https, host=${appLinkHost}, path=/auth/confirm und flutter_deeplinking_enabled=true; Gradle-Property gesetzt; Entitlements-Datei existiert und enthält applinks:$(APP_LINK_HOST); CODE_SIGN_ENTITLEMENTS vor C1 leer bzw. nur über die Weiche gesetzt, nach C1 in allen Build-Konfigurationen (der Test prüft den jeweils gültigen Stand); Info.plist FlutterDeepLinkingEnabled=true; AASA und assetlinks sind gültiges JSON, Bundle-/Package-ID stimmen mit Xcode-Projekt bzw. applicationId überein, Pfad ist genau /auth/confirm; xcconfig-Host = Gradle-Host – Release-Checkliste (7.3) – A2, T2
Logging Test: Router-Fehler- und Logpfade erhalten bei einem /auth/confirm-URI keinen token_hash (z. B. AppLogger mit Test-Handler, Route mit Token aufrufen, Logausgabe prüfen) – – – T2

7.2 Manuelle Gerätematrix (Profile- oder Release-Build, nach C1–C6)

Je Plattform (iOS-Gerät, Android-Gerät bzw. Emulator mit Play-Services) und jeweils kalt und warm:

# Fall Erwartung
M1 Bestätigungslink in Apple Mail (iOS) bzw. Gmail (Android) App öffnet /auth/confirm, Knopf, angemeldet in der App
M2 Bestätigungslink in Gmail-App (iOS) App öffnet oder Fallback Web (dokumentieren, welches)
M3 Bestätigungslink in Outlook (beide) voraussichtlich Fallback Web wegen Safe Links (dokumentieren, welches)
M4 App läuft im Hintergrund auf einer anderen Seite App kommt nach vorn, /auth/confirm
M5 Reset-Link App, Knopf, „Neues Passwort“, Erfolg, andere Sitzung wird abgemeldet (KTO-01 Krit. 9)
M6 Reset-Link zweimal benutzt zweites Mal „Link abgelaufen oder schon benutzt“
M7 App nicht installiert Web-App schließt den Schritt ab
M8 Link am PC geöffnet, Registrierung war am Handy Web-App am PC bestätigt, Anmeldung in der App danach möglich
M9 iOS: Nutzer wählt „In Safari öffnen“ Web-App; beim nächsten Link bleibt iOS beim Browser (bekanntes iOS-Verhalten, dokumentieren)
M10 Android 7–11 (falls minSdk < 31 bleibt, F11) App öffnet nach Verifizierung bzw. Auswahldialog; Verhalten dokumentieren

Ergebnis als Tabelle im PR B bzw. im Issue #49; Abweichungen (z. B. M2, M3) werden beschrieben, nicht als Fehler gewertet, solange der Fallback greift.

7.3 Ergänzung Release-Checkliste (docs/auth-konfiguration.md § 5)

  • curl -sI https://<Domain>/.well-known/apple-app-site-association und /.well-known/assetlinks.json: Status 200, keine Weiterleitung, Content-Type: application/json, Inhalt identisch zum Repo (APPLINK-05).
  • Apple-CDN-Sicht: https://app-site-association.cdn-apple.com/a/v1/<Domain> liefert die aktuelle Datei.
  • Google-Prüfung: Digital Asset Links API (https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://<Domain>&relation=delegate_permission/common.handle_all_urls) listet Paket und Fingerabdrücke.
  • GOTRUE_SITE_URL-Host = APPLINK-01.
  • Gerätematrix 7.2 auf der Release-Build durchgeführt.

8. Aufgaben

8.1 PR A – Einstieg (feature/49-anmeldung-einstieg, Refs #49)

# Agent Aufgabe abhängig von
U1 ui-designer Gestaltung SignInPrompt (Karte, Primär- und Sekundärknopf, Abstände, breite Bildschirme), Sekundärknopf-Variante in core/widgets falls nötig, Textvorschläge de/en, a11y (Überschrift als Header, Fokusreihenfolge Web) –
A1 flutter-dev SignInPrompt, showSignInPromptProvider, HomeScreen als ConsumerWidget (5.1), ARB-Schlüssel de/en, build_runner U1
T1 test-engineer Unit-Test Provider, Widget-Tests HomeScreen/Prompt (7.1 Krit. 1), Playwright web: Prompt und Navigation A1
# Agent Aufgabe abhängig von
S1 flutter-dev Spike (max. ½ Tag), Ergebnis als Änderungsprotokoll in dieser Spec. Zu klären: (a) Flutter 3.47: Ist Deep-Linking ohne Flag aktiv, und übergibt die Engine Pfad und Query an go_router (Kaltstart, warm)? (b) iOS mit UIScene (SceneDelegate.swift, FlutterSceneDelegate): kommen scene(_:willConnectTo:) und scene(_:continue:) mit NSUserActivityTypeBrowsingWeb bei go_router an, während das app_links-Plugin registriert ist? (c) Android mit singleTop: onNewIntent erreicht go_router? (d) Verarbeitet supabase_flutter die Links tatsächlich nicht (kein getSessionFromUrl)? (e) Kopiert flutter build web web/.well-known/ in build/web/, und liefert serve -s bzw. das geplante Hosting die AASA mit application/json aus? (f) Ersetzt go_router bei warmem Link den Stack (go) oder legt er ihn oben drauf? Prüfweg: Android-Emulator per adb … am start (6.2), iOS-Simulator mit xcrun simctl openurl mit einem eigenen Test-Schema nur im lokalen Spike-Branch (nicht committen), um den Weg bis go_router zu sehen; Universal-Link-Zuordnung selbst ist ohne C1 nicht prüfbar. –
A2 flutter-dev Plattform-Dateien nach 5.2 (Manifest-Intent-Filter, Gradle-Property, Entitlements, xcconfig, Info.plist-Flag; CODE_SIGN_ENTITLEMENTS erst nach C1 bzw. über die xcconfig-Weiche, E6); Logging-Prüfung (5.2); falls S1 negativ: Rückfall-Adapter nach 5.4 (vorher Spec-Ergänzung). Pflicht vor dem PR (lokal, die CI baut nur Web): flutter build apk --release und flutter build ios --no-codesign, wenn möglich zusätzlich ein Gerätebuild mit Signatur; Ergebnis im PR vermerken S1
D1a devops AASA- und assetlinks-Vorlagen nach 4.3 am Ort aus S1 (e); Doku-Abschnitt APPLINK-01 bis -06 in docs/auth-konfiguration.md; Release-Checkliste 7.3; Gateway-Vorgaben (Auslieferung ohne Redirect, application/json, /.well-known/ vom SPA-Rewrite ausgenommen, öffentlich erreichbar, nicht hinter VPN/Basic-Auth) als Ergänzung zu D1 aus Spec #1; Android-Release-Signatur vorbereiten (Upload-Keystore außerhalb des Repos; prüfen, dass key.properties, *.jks und *.keystore weiter ignoriert werden, das steht bereits in .gitignore und app/android/.gitignore; Signing-Config in build.gradle.kts), ohne Schlüssel selbst anzulegen S1
T2 test-engineer Konfigurations-Test (7.1 APPLINK), Widget-Tests Deep Link kalt/warm (Krit. 2, 3), Logging-Test, Regression Playwright; lokale Builds wie bei A2 nachvollziehen; Vorlage der Gerätematrix 7.2 im PR A2, D1a, A1 (PR A gemergt, wegen „Prompt verschwunden“ in 7.1 Krit. 2)
W1 docs-writer Outline-Seite KTO-01 bzw. neue Seite „App Links“ (Ablauf, Fallback, Grenzen) nach Merge A2, D1a
W2 architect / docs-writer Ablösung in Spec #1 dokumentieren: § 10 Eintrag „E4 und § 6 (Mail-Links, Autofill) durch Spec #49 E2/F2 abgelöst“, Verweise an E4 (Zeile „E4“ in § 1) und § 6 (Zeilen „Mail-Links“ und „Autofill / Passwortmanager“), jeweils „Abgelöst durch Spec #49 E2“ bzw. „siehe Spec #49 F2“. Nur Verweise und Änderungsprotokoll, keine Inhaltsänderung. Noch offen: Im Review-Commit nicht enthalten, weil der Commit über die Forgejo-API läuft und Spec #1 dafür vollständig neu übertragen werden müsste (rund 50.000 Tokens, fehleranfällig). Erledigt der nächste Agent mit Shell-Zugriff auf diesem Branch, spätestens mit PR A. –

8.3 Aufgaben, die nur Chris erledigen kann

# Aufgabe Warum Chris blockiert
C1 Team bestätigen: Das Xcode-Projekt nutzt bereits DEVELOPMENT_TEAM = KSX6GJY9C5 (Debug, Release, Profile). Chris bestätigt, ob das ein bezahltes Apple-Developer-Program-Team ist. Wenn ja: KSX6GJY9C5 wird in APPLINK-03 und der AASA-Vorlage eingetragen, „Associated Domains“ für die App-ID aktiviert und CODE_SIGN_ENTITLEMENTS verdrahtet (E6). Wenn nein (kostenloses Personal Team): Beitritt zum Programm (kostenpflichtig) oder iOS-Teil zurückstellen; bis dahin bleiben die Entitlements unverdrahtet Vertrag, Zahlung, Identität iOS-Teil von Krit. 2/3, M1–M9 iOS, Verdrahtung CODE_SIGN_ENTITLEMENTS
C2 Google Play Console einrichten, Play App Signing aktivieren, SHA-256 des App-Signing-Schlüssels mitteilen; Upload-Keystore erzeugen und sicher verwahren (nicht im Repo), SHA-256 mitteilen Konto, Schlüsselhoheit Android-Teil von Krit. 2/3 in Release-Builds
C3 Bundle-ID / Application-ID endgültig festlegen (heute Platzhalter work.cshomelabs.shuffle, R15 aus Spec #1) Nach Store-Veröffentlichung nicht mehr änderbar; AASA und assetlinks hängen daran C1, C2, finale Dateien
C4 Produktions-Domain endgültig festlegen (heute Platzhalter shuffle.cshomelabs.work, F8 aus Spec #1) und sicherstellen, dass sie öffentlich per HTTPS erreichbar ist (kein VPN, keine Zugangssperre vor /.well-known/) Domain und DNS gehören Chris APPLINK-01, -05
C5 Auf Prod: Zuordnungsdateien ausliefern (Werte aus C1–C3), GOTRUE_SITE_URL auf diese Domain setzen, Release-Checkliste 7.3 abhaken Zugriff auf Server und Prod-Konfiguration nur Chris (kein Agent greift auf Server zu) Krit. 2/3 Ende-zu-Ende
C6 Gerätematrix 7.2 auf echten Geräten durchführen oder abnehmen (iPhone, Android) Physische Geräte, Apple-ID Abschluss #49
C7 Offene Fragen F1–F11 entscheiden, Spec freigeben Freigabe Umsetzung

8.4 Reihenfolge

sofort      PR A: U1 → A1 → T1                     (unabhängig von allem anderen)
parallel    S1 (Spike) → A2, D1a → (PR A gemergt) → T2 → PR B (mergebar mit Platzhaltern, iOS-Entitlements unverdrahtet bis C1)
Chris       C1 (Team bestätigen), C3, C4 → C2 → C5 → C6 → #49 schließen (F7)
danach      W1; nach C1: Verdrahtung CODE_SIGN_ENTITLEMENTS (kleiner Folge-PR, flutter-dev)

PR B ändert HomeScreen nicht. Die Umsetzung von A2 ist unabhängig von PR A; der Widget-Test zu Krit. 2 („Prompt verschwunden“) braucht PR A, deshalb wird PR B nach PR A gemergt. Kein Backend-PR nötig (backend-dev hat in #49 keine Aufgabe).

9. Risiken & offene Fragen

9.1 Risiken

# Risiko Auswirkung Gegenmaßnahme
R1 Mail-Programme und Weiterleitungsdienste (voraussichtlich Outlook Safe Links, Tracking-Redirects, In-App-Browser) lösen Universal/App Links nicht aus Link öffnet die Web-App statt der App Fallback Web ist vollwertig (Krit. 4); Gerätematrix dokumentiert das Verhalten; kein Tracking-Redirect in eigenen Mails (bereits so, Spec #1 § 4.1)
R2 SPA-Rewrite liefert index.html statt der AASA aus, oder es gibt eine Weiterleitung (z. B. http→https, www) Universal/App Links fallen still aus APPLINK-05, Release-Checkliste 7.3 mit curl; /.well-known/ ausdrücklich vom Rewrite ausnehmen (D1a)
R3 Apple lädt die AASA über sein CDN und cacht sie (Stunden bis Tage); Android prüft assetlinks.json vor allem bei der Installation Änderungen wirken verzögert; eine bei Installation fehlende Datei lässt die App-Link-Prüfung scheitern Dateien vor der ersten Store-Installation ausliefern; iOS-Entwicklermodus (?mode=developer) für Tests; Android: pm verify-app-links --re-verify
R4 Signatur passt nicht zu assetlinks.json (heute release mit Debug-Schlüssel, Play App Signing signiert neu) App Links auf Android ohne Wirkung Beide Fingerabdrücke eintragen (C2); Release-Signatur vorbereiten (D1a); Prüfung über die Digital Asset Links API
R5 Bundle-/Application-ID oder Domain ändern sich nach Veröffentlichung Dateien und Builds müssen neu, alte Installationen öffnen keine Links mehr C3, C4 vor dem ersten Store-Build; Werte nur an einer Stelle (E8)
R6 Ein späterer, breiterer Pfad-Umfang zieht Web-Adressen ungewollt in die App Nutzer landen in der App, wo die Seite fehlt (404) Genau /auth/confirm (E2); Erweiterung nur mit Entscheidung (F1)
R7 token_hash gelangt in Logs (App-Logger, Router-Diagnose, Android-System-Log bei am start) Einmal-Token lesbar, beim Reset Kontoübernahme bis zum Ablauf (1 h) Logging-Test (7.1); debugLogDiagnostics aus; Token ist einmalig und läuft nach 1 h ab (AUTH-CFG-07); adb-Aufrufe nur lokal
R8 Flutter-Deep-Linking und das transitive app_links-Plugin stören sich (iOS UIScene), oder eine künftige supabase_flutter-Version wertet token_hash selbst aus Links kommen nicht bei go_router an oder werden doppelt eingelöst (E3 verletzt) S1; Rückfall 5.4; bei Updates von supabase_flutter _defaultIsAuthCallbackDeeplink prüfen (Hinweis im PR). Als weitere Option bleibt detectSessionInUri: false in Supabase.initialize. Wir nutzen weder OAuth noch PKCE-Redirects, die Option wirkt aber auch im Web; sie wird deshalb nur mit eigener Prüfung eingesetzt.
R9 Homelab-Domain (cshomelabs.work) ist nicht öffentlich oder hinter einer Zugangssperre Apple-CDN und Google erreichen die Dateien nicht C4; APPLINK-05
R10 Warmer Deep Link ersetzt den Navigationsstack Nicht gespeicherte Eingaben auf dem vorherigen Screen gehen verloren Akzeptiert: Mail-Links sind ein bewusster Wechsel; S1 (f) bestätigt das Verhalten
R11 iOS-Debug-Builds starten nicht kalt über einen Link Fehlalarm beim Testen Gerätetests mit Profile/Release (6.1)
R12 Bestätigung im mobilen Browser (Fallback) meldet den Nutzer im Browser an, nicht in der App Nutzer muss sich in der App zusätzlich anmelden Bestehender Hinweis „Du kannst dich jetzt auch in der App anmelden.“; die Startseite der App bietet den Einstieg (Krit. 1)
R13 Abfangschutz nur eingeschränkt: App Links schützen vor fremden Apps nur bei erfolgreicher Verifizierung und verlässlich erst ab Android 12. minSdk = flutter.minSdkVersion schließt Android 7–11 ein; dort kann bei fehlender Verifizierung eine andere App mit passendem Intent-Filter den Link (und damit den token_hash) empfangen. Auf iOS greift die Zuordnung nur nach erfolgreichem Laden der AASA Bei einem Reset-Link wäre das eine Kontoübernahme bis zum Ablauf E3 aus Spec #1 begrenzt den Schaden: Der Token wird erst per Knopfdruck eingelöst, ist einmalig und läuft nach 1 h ab (AUTH-CFG-07); eine neue Anforderung ersetzt ihn. Verifizierung in der Release-Checkliste (7.3). Option: minSdk ≥ 31 (F11)
R14 Login-CSRF: Wer einem Opfer einen eigenen, gültigen Bestätigungs- oder Reset-Link unterschiebt, kann es nach dem Knopfdruck im Angreifer-Konto anmelden (gilt in App und Web gleichermaßen, besteht schon heute im Web) Opfer erfasst Daten im falschen Konto Bewusst akzeptiert (F3): Der Knopfdruck auf einer Seite mit klarem Titel ist eine bewusste Handlung; der Schaden ist gering (Angreifer sieht, was das Opfer im fremden Konto anlegt). Kein zusätzlicher Dialog ohne zweiten Anwendungsfall

9.2 Offene Fragen (Entscheidung Chris)

# Frage Empfehlung betrifft
F1 Welche Pfade beansprucht die App? Nur /auth/confirm oder alle Web-Adressen? Nur /auth/confirm. Erweiterung (z. B. Spieldetail-Links) erst, wenn es teilbare Inhalte gibt und die App die Route kennt. E2, APPLINK-02, R6
F2 Passwortspeicher von App und Web verbinden (webcredentials iOS, get_login_creds Android)? Ja für iOS (webcredentials, gleiche Datei, kein Mehraufwand außer Entitlement-Zeile; Passwort-Vorschläge der Web-Anmeldung erscheinen in der App). Android get_login_creds optional, nein in #49. 4.3, 5.2
F3 Was passiert, wenn ein Link geöffnet wird, während in der App ein anderes Konto angemeldet ist? Wie im Web: Nach Knopfdruck gilt die neue Sitzung, kein zusätzlicher Dialog. Der Knopfdruck ist bereits eine bewusste Handlung; ein Dialog wäre neue UX ohne zweiten Anwendungsfall. Das damit verbundene Login-CSRF-Restrisiko (R14) wird bewusst akzeptiert; es besteht schon heute im Web. 5.2, R14
F4 Braucht die App vor KTO-11 ein vorläufiges „Abmelden“ (z. B. für Gerätetests)? Nein. Abmelden ist KTO-11 Kriterium 3; Tester installieren neu oder nutzen den Reset-Abbruch. 5.1
F5 Eigene Staging-App-Builds mit Staging-Host (und Debug-Fingerabdruck in der Staging-assetlinks.json)? In #49 nein. Ein Host je Build über die zentrale Property (E8); Staging-Builds entscheidet devops mit dem Hosting-Setup. Bis dahin Android lokal per adb (6.2) und iOS erst gegen Prod/Staging-Domain mit Entwicklermodus. E8, APPLINK-01
F6 Erscheint der Hinweis zur Anmeldung auch im Web? Ja. QUE-09 Kriterium 13 verlangt ihn für App und Web-Adresse; „Web unverändert“ gilt nur für die Mail-Links. E1
F7 Wann wird #49 geschlossen: mit dem Merge von PR B oder erst nach der Gerätebestätigung (C6)? Nach C6. PR B referenziert Refs #49; Chris schließt #49 nach der Gerätematrix. Alternativ: PR B schließt #49, Gerätetest wird ein eigenes Issue. 8.2
F8 Wo liegen die Zuordnungsdateien im Repo? app/web/.well-known/, wenn S1 (e) zeigt, dass Build und Hosting sie korrekt ausliefern; sonst deploy/well-known/ mit Gateway-Regel. E7, D1a
F9 Für das Produktions-Setup (D1 aus Spec #1, ADR-0002) gibt es kein Forgejo-Issue. Sollen C4/C5 und D1a dort hinein? Ja: Issue „Produktions-Setup nach ADR-0002“ anlegen (requirements-analyst oder devops), Chris-Aufgaben C4/C5 und die Gateway-Vorgaben dort bündeln; #49 verweist darauf. 8.3
F10 Übernimmst du die Präzisierungen der Akzeptanzkriterien (§ 2, Vorschläge zu 1–4 und Kriterium 5)? Ja, sie machen Kriterien 2–4 testbar und schreiben das Web-Verhalten fest. Danach Issue-Text anpassen. § 2
F11 minSdk auf 31 (Android 12) anheben, damit App Links nur mit verlässlichem Abfangschutz laufen? Nein in #49. Android 7–11 schließt sonst Nutzer aus; E3 (Knopfdruck, einmalig, 1 h) begrenzt den Schaden (R13). Neu bewerten, wenn die Store-Statistik kaum Geräte unter Android 12 zeigt. R13, M10

10. Änderungsprotokoll

Datum Änderung
2026-11-03 Entwurf (architect)
2026-11-03 Review code-reviewer eingearbeitet: vorhandenes Team KSX6GJY9C5 (C1, APPLINK-03), CODE_SIGN_ENTITLEMENTS erst nach C1 bzw. über xcconfig-Weiche, E6 eingeschränkt, lokale Builds als Pflicht (A2, T2); Ablösung von Spec #1 E4/§ 6 in § 1 und als Aufgabe W2 dokumentiert (Verweise in Spec #1 selbst noch offen), Abweichung vom Issue-Vorschlag (/reset-password) festgehalten; email_change braucht Ergänzung in KTO-11 (E2); belegte Fakten aus dem Quelltext von Spike-Annahmen getrennt (E5, § 5.2); Outlook einheitlich „voraussichtlich“; QUE-06 Krit. 4 in der Matrix, T2 abhängig von A1; Abfangschutz eingeschränkt (R13, F11, M10), Login-CSRF akzeptiert (R14); ?mode=developer-Mechanismus, HomeScreen liest nur den Provider, Import home → auth begründet, key.properties nur prüfen, Quelle Debug-Kaltstart