Merge pull request 'docs(auth): Spec für Anmelde-Einstieg und App Links (Issue #49)' (#55) from docs/49-mobil-login-spec into main
Some checks are pending
CI / flutter (push) Waiting to run
CI / web-build (push) Waiting to run
CI / supabase (push) Waiting to run
CI / supabase-tests (push) Waiting to run
CI / e2e (push) Blocked by required conditions

Reviewed-on: #55
This commit is contained in:
cschlaefke 2026-11-04 00:29:41 +00:00
commit 220dfd195d
2 changed files with 368 additions and 3 deletions

View file

@ -22,7 +22,7 @@ Gleichzeitig ist #1 die **erste Story mit App-Code**. Das Flutter-Gerüst existi
| E1 | **App-Fundament als eigenes Chore-Issue #31** vor #1, nicht als Teil von #1 (entschieden, F10) | KAT-01/KAT-02 (R1.1) brauchen das Gerüst auch ohne Anmeldung; kleinere, prüfbare PRs; CLAUDE.md verlangt ein Issue je Änderung. `datenmodell-r1.md` Schritt 4 bündelt Gerüst und Anmeldung. Wir weichen davon nur im Zuschnitt ab, die Reihenfolge bleibt gleich. |
| E2 | **Ein Issue #1, drei PRs** (01a Registrierung, 01b Anmelden, 01c Passwort zurücksetzen), davor ein Backend-PR (Migration + Auth-Konfiguration) | Die Kriterien teilen Backend-Konfiguration und Domänenschicht. Getrennte Issues würden Abhängigkeiten nur verschieben. Die PRs referenzieren `Refs #1`, der letzte `Closes #1`. **Abweichung vom Issue-Vorschlag:** Krit. 6 liegt in 01b statt 01a, weil der Hinweis auf die ausstehende Bestätigung beim *Anmelden* erscheint und den LoginScreen braucht. Die Aktion „E-Mail erneut senden“ entsteht schon in 01a (CheckEmailScreen) und wird in 01b wiederverwendet. |
| E3 | **Links in Mails zeigen auf eine App-Seite `/auth/confirm?token_hash=…&type=…`**, nicht auf `{{ .ConfirmationURL }}`. Die Seite löst `verifyOTP` erst per Knopfdruck aus. | Funktioniert geräteübergreifend: kein PKCE-Verifier nötig, z. B. Registrierung am Handy und Mail am PC. Mail-Scanner (z. B. Safe Links) verbrauchen den Einmal-Link nicht. Die Einmaligkeit bleibt serverseitig erhalten (Kriterium 8). |
| E4 | **In R1 landen Mail-Links immer in der Web-App.** Universal Links/App Links (iOS/Android) sind nicht Teil von #1. | Die Web-App kann alle Schritte ausführen. Universal Links brauchen AASA/assetlinks-Hosting und Store-Signaturen; das ist eine eigene Aufgabe und sollte ein Folge-Issue werden. |
| E4 | *(Abgelöst durch Spec #49 E2.)* **In R1 landen Mail-Links immer in der Web-App.** Universal Links/App Links (iOS/Android) sind nicht Teil von #1. | Die Web-App kann alle Schritte ausführen. Universal Links brauchen AASA/assetlinks-Hosting und Store-Signaturen; das ist eine eigene Aufgabe und sollte ein Folge-Issue werden. |
| E5 | **Mailsprache über `user_metadata.locale`**: Je Mailtyp gibt es *eine* Vorlage mit Go-Template-Verzweigung de/en (Standard Englisch, F6). Ein neuer DB-Trigger hält `auth.users.raw_user_meta_data.locale` synchron zu `profiles.locale`. | GoTrue kennt nur eine Vorlage je Typ und stellt `.Data` (= `user_metadata`) bereit. Die Profilsprache ist maßgeblich (Kriterium 11, KTO-11 Kriterium 2). Ein Send-Email-Hook (Edge Function + eigener SMTP-Versand) wäre mehr bewegliche Teile ohne zweiten Anwendungsfall. Spike B0 hat bestätigt, dass Betreff und Text `.Data.locale` auswerten (Spike-Dokument (a)); ein Rückfall ist nicht nötig. |
| E6 | **Validierung zweistufig**: Feldfehler im Client (Kriterien 2, 3, 10). Der Server erzwingt dieselben Regeln als Schutz (Trigger, `minimum_password_length = 10`). | Der Client gibt sofort Rückmeldung am Feld. Der Server vertraut nie dem Client. GoTrue meldet Trigger-Fehler als HTTP 500; der Body enthält zwar die Meldung des Triggers (z. B. `display_name_invalid`, Spike A4; gilt auch für U+200B im Anzeigenamen), das Format ist aber nicht vertraglich zugesichert. Der Client bleibt deshalb die nutzersichtbare Stelle. |
| E7 | **„Liste häufiger Passwörter“** wird nur im Client als gebündelte Liste geprüft; der Server erzwingt die Mindestlänge 10. **Kein Have I Been Pwned** (entschieden, F2): `GOTRUE_PASSWORD_HIBP_ENABLED=false`. | HIBP würde einen SHA-1-Präfix an einen US-Dienst (Cloudflare/HIBP) übertragen (Klärung 21/47). Außerdem gibt es HIBP in der lokalen Supabase CLI nicht. Restrisiko: Über die rohe API lassen sich häufige Passwörter mit mindestens 10 Zeichen setzen (R14). |
@ -328,8 +328,8 @@ Alle Pakete unterstützen iOS, Android und Web. `flutter-dev` pinnt die Versione
| Thema | Web | iOS / Android |
|---|---|---|
| Sitzung (Krit. 5) | `localStorage`; bleibt nach Reload und Browser-Neustart erhalten. Kein Abgleich zwischen Tabs: ein zweiter Tab merkt die Abmeldung erst beim nächsten Refresh. | SharedPreferences (App-Sandbox); bleibt nach Neustart erhalten. Bei Neuinstallation ist die Sitzung weg. |
| Mail-Links (E3, E4) | `/auth/confirm` braucht `usePathUrlStrategy` **und** eine SPA-Rewrite-Regel im Webserver (alle Pfade → `index.html`, Aufgabe D1). | Links öffnen in R1 den Browser (Web-App). Universal/App Links kommen in einem Folge-Issue. |
| Autofill / Passwortmanager | `AutofillHints.email`, `newPassword`, `password`; Flutter Web unterstützt Passwortmanager nur eingeschränkt (bekannt, im PR vermerken) | Autofill iOS/Android inkl. „Starkes Passwort vorschlagen“; Associated Domains erst mit Universal Links |
| Mail-Links (E3, E4; E4 abgelöst durch Spec #49 E2) | `/auth/confirm` braucht `usePathUrlStrategy` **und** eine SPA-Rewrite-Regel im Webserver (alle Pfade → `index.html`, Aufgabe D1). | Links öffnen in R1 den Browser (Web-App). Universal/App Links kommen in einem Folge-Issue. |
| Autofill / Passwortmanager | `AutofillHints.email`, `newPassword`, `password`; Flutter Web unterstützt Passwortmanager nur eingeschränkt (bekannt, im PR vermerken) | Autofill iOS/Android inkl. „Starkes Passwort vorschlagen“; Associated Domains erst mit Universal Links (siehe Spec #49 F2) |
| Tastatur | Tab-Reihenfolge = Lesereihenfolge, Enter sendet ab, sichtbarer Fokus (QUE-06 Krit. 4) | `TextInputType.emailAddress`, `TextInputAction.next/done` |
| Offline | Login/Registrierung/Reset brauchen Verbindung → Hinweis statt technischem Fehler (Querschnitt Zustände 2). Kein Connectivity-Paket; der Fehler wird beim Aufruf erkannt. | gleich |
| Layout (Plattformen Krit. 2) | ab 600 dp zentrierte Karte, max. 480 dp breit | Vollbild, scrollbar (200 % Schrift) |
@ -533,3 +533,7 @@ Nach der Freigabe am 2026-11-02 eingearbeitet. Die Texte der Akzeptanzkriterien
### 2026-11-03 – A11y-Fix Semantics-Split (Issue #41, Branch `fix/41-semantics-split`)
1. **Ursache und Fix:** Die `Card` im Layout ab 600 dp faltete Titel, Text und Knopf in einen Semantics-Knoten. `AppFormScaffold` setzt `Semantics(explicitChildNodes: true)` um den Karteninhalt (Regel in `docs/design/auth-screens.md`). Der Befund aus T3 (A11y-Befund QUE-06) ist damit behoben; `pressFirstButton` ist entfernt.
### 2026-11-03 – Verweis auf Spec #49 (Issue #49)
E4 und § 6 (Mail-Links, Autofill) sind durch Spec #49 E2/F2 abgelöst: Universal Links (iOS) und App Links (Android) für `/auth/confirm` werden dort spezifiziert. Der Inhalt dieser Spec ist sonst unverändert.

View file

@ -0,0 +1,361 @@
# 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):
```json
{
"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`:
```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.
### 5.2 PR B: App Links (Kriterien 2–4)
**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 |
### 8.2 PR B – App Links (`feature/49-app-links`, `Refs #49`; #49 wird erst nach der Gerätebestätigung geschlossen, siehe F7)
| # | 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 |