# Auth-Screens: Layout-Skizzen und Texte (Spec #1, Task U1) Grundlage: `docs/specs/1-auth-registrierung-anmeldung.md` (§ 4.4 Meldungen, § 5.4 Screens, § 6 Plattform). Komponenten: `app/lib/core/widgets/` (siehe `docs/design-system.md`). Alle Texte unten sind **Vorschläge** und gehen über l10n (ARB); die ARB-Dateien pflegt der flutter-dev (A1/A2). Schlüssel sind `lowerCamelCase` mit Präfix `auth`. ## 1. Gemeinsames Layout - Alle sechs Screens nutzen `AppFormScaffold`. Kompakt (< 600 dp): Vollbild, scrollbar, Seitenrand 16 dp. Ab 600 dp: zentrierte Karte, max. 480 dp breit (`AppBreakpoints.authCardMaxWidth`), Innenabstand 24 dp. - Inhalt in einer Spalte, Abstand zwischen Feldern 16 dp, zwischen Gruppen 24 dp. - Reihenfolge = Lesereihenfolge = Tab-Reihenfolge: Titel, Banner (falls vorhanden), Felder, Checkboxen, Primärknopf, Sekundärlinks. - Titel: `headlineSmall`, darunter optional ein Einleitungssatz in `bodyMedium`. Titel als Semantics-Überschrift (`Semantics(header: true)`). - Banner (`AppBanner`) stehen **über** den Feldern, direkt unter dem Titel. Fehler am Feld stehen unter dem Feld. - Genau ein Primärknopf (`AppPrimaryButton`, volle Breite). Sekundäraktionen sind `TextButton`s (48 dp). - Eingaben bleiben bei jedem Fehler erhalten. Während `submitting` sind Knopf und Felder gesperrt, der Knopf zeigt den Spinner und den gesprochenen Text `authSubmitting`. - Nach einem Fehler setzt der Screen den Fokus auf das erste fehlerhafte Feld; bei Banner-Fehlern bleibt der Fokus am Knopf, das Banner wird als Live-Region angesagt. - Enter im letzten Feld sendet ab (`TextInputAction.done`), sonst `next`. ### Semantics-Struktur (Regel, Issue #41) - Überschrift, Text und Knopf sind **getrennte Semantics-Knoten**: `heading` (`AuthScreenTitle`), Text, `button` mit Label (`AppPrimaryButton`, `TextButton`). Das gilt in jeder Breite, auch in der Karte ab 600 dp. - Ursache des Fehlers: `Card` ist ein Semantics-Container ohne `explicitChildNodes`. Alle Kinder ohne eigenen Container (Titel, `Text`, Knopf) werden in dessen einen Knoten gefaltet. Seiten mit Eingabefeld fielen nicht auf, weil `AppTextField` & Co. eigene Container sind. Im Web und ab 600 dp trat es auf, auf dem Handy (ohne Karte) nicht. - Deshalb kapselt `AppFormScaffold` den Karteninhalt in `Semantics(explicitChildNodes: true)`. Neue Layouts mit `Card`/`Semantics(container: true)` um Seiteninhalt müssen das ebenfalls tun; `MergeSemantics` nur gezielt für zusammengehörige Einzelelemente, nie um ganze Seiten. - Banner bleiben eigene Live-Regionen (`AppBanner`), ihre Aktion ist ein eigener Knopf-Knoten. - Test: `expectSeparateSemantics` (`app/test/features/auth/presentation/auth_ui_support.dart`) im weiten Layout (`wideScreenSize`); E2E klickt per `getByRole('button', …)`. Legende der Skizzen: `[ ]` Feld, `[x]` Checkbox, `( )` Primärknopf, `<…>` Link/TextButton, `!` Banner. ## 2. Skizzen ### RegisterScreen (`/register`) ``` Konto erstellen (Titel) ! Fehler-Banner (nur bei registrationRejected / network / rateLimited / unexpected) E-Mail [ name@example.com ] Fehler: authEmailInvalid Passwort [ •••••••••• (Auge) ] Hinweis: authPasswordRule / Fehler je Regel Anzeigename [ Alex 4/30 ] Fehler: authDisplayNameEmpty | TooLong | Invalid [x] Ich bin mindestens 16 Jahre alt Fehler: authAgeNotConfirmed [x] Ich habe die gelesen und stimme den zu Fehler: authTermsNotAccepted ( Konto erstellen ) ``` ### CheckEmailScreen (`/register/check-email`, Modus Bestätigung bzw. Reset) ``` Prüfe dein Postfach (Titel) Wir haben dir eine E-Mail geschickt, falls ... (Text, bodyLarge) Der Link ist 1 Stunde gültig. Die E-Mail kann einige Minuten dauern. Schau auch im Spam-Ordner nach. ! Info-Banner "E-Mail wurde erneut gesendet" (nach Resend, auch bei emailSendLimited) ! Fehler-Banner bei rateLimited / network (nur wenn E-Mail fehlt, z. B. nach Reload:) E-Mail [ ] ( E-Mail erneut senden ) -> nur Modus Bestätigung, gesperrt solange Anfrage läuft (Modus Reset: statt des Resend-Knopfs -> /forgot-password) ``` Der Modus steckt im **Pfad**, nicht im `extra`: `/register/check-email` (Bestätigung, E-Mail als `extra`) und `/forgot-password/check-email` (Reset, ohne `extra`). So überlebt der Modus einen Reload im Web. Im Modus Reset gibt es keinen Resend (Spec § 5.4), Text `authCheckEmailBodyReset`, Gültigkeit und Zusatz „kann einige Minuten dauern“ wie im Modus Bestätigung; Keys `check_email_different_address` (statt `check_email_resend`). ### LoginScreen (`/login`, umgesetzt in 01b) ``` Anmelden ! Fehler-Banner: authErrorInvalidCredentials | authErrorRateLimited | authErrorNetwork (+ "Erneut versuchen") ! Info-Banner authErrorEmailNotConfirmed + Aktion "Bestätigungs-E-Mail erneut senden" ! Info-Banner "Wir haben die E-Mail erneut gesendet ..." | Fehler-Banner (authErrorRateLimited / authErrorNetwork) nach dem Resend E-Mail [ ] Fehler (nur lokal): authErrorEmailInvalid Passwort [ ••••• (Auge) ] Fehler (nur lokal): authPasswordRequired ( Anmelden ) -> /forgot-password -> /register ``` Umsetzungsentscheidungen: - Falsche Zugangsdaten stehen nie am Feld, sondern nur im Banner; Text für falsche E-Mail und falsches Passwort ist identisch (Krit. 7, Test vergleicht alle sichtbaren Texte). Felder zeigen nur lokale Fehler (leer / E-Mail-Syntax). - Bei `emailNotConfirmed` merkt sich der Controller die E-Mail des Versuchs; "erneut senden" nutzt diese Adresse (`ResendController`, `ResendOrigin.login`), nicht den aktuellen Feldinhalt. Dort wird `emailSendLimited` als Drosselungshinweis gezeigt (F12). Während des Sendens ist die Aktion **deaktiviert, aber sichtbar** (`AppBanner.actionEnabled: false`): kein Doppelversand, und der Fokus eines Screenreaders verschwindet nicht mit dem Knopf (Semantics: Knopf, nicht aktiviert). - Ein neuer Anmeldeversuch setzt das Resend-Ergebnis zurück. - `from` wird an „Konto erstellen“ und „Passwort vergessen?“ weitergegeben (`authLocation`, geprüft mit `safeFromPath`); Register- und ForgotPassword-Screen geben es mit „Anmelden“ bzw. „Zurück zur Anmeldung“ an `/login` zurück. - Erfolg: `context.go()`. `from` wird mit `safeFromPath` geprüft (nur interne Pfade, keine Auth-Seiten, kein `//`, `/\` oder Schema); sonst Startseite. Zusätzlich leitet der Router bei Status `signedIn` von `/login` um. - Autofill: `AutofillGroup`, E-Mail `username` + `email`, Passwort `password`; `TextInput.finishAutofillContext()` bei Erfolg. - Neue l10n-Schlüssel: `authLoginTitle`, `authLoginSubmit`, `authLoginForgotPassword`, `authLoginCreateAccount`. - Widget-Keys: `login_screen`, `login_email`, `login_password`, `login_submit`, `login_banner`, `login_not_confirmed_banner`, `login_resent_banner`, `login_resend_error_banner`, `login_forgot_link`, `login_register_link`. ### ForgotPasswordScreen (`/forgot-password`) ``` Passwort zurücksetzen Gib die E-Mail-Adresse deines Kontos ein. Wir senden dir einen Link zum Zurücksetzen. E-Mail [ ] Fehler: authEmailInvalid ( Link senden ) -> /login (mit from) ``` Umsetzung: Nach jedem 200 **und** bei `emailSendLimited` (F12) geht es ohne Hinweis zu `/forgot-password/check-email` (Antwort für bekannte und unbekannte Adresse sowie für die Mail-Sperre ist identisch, getestet per exaktem Textvergleich). Sichtbare Fehler nur für lokale Syntax, `invalidEmail` (am Feld), `rateLimited`/`network`/`unexpected` (Banner, bei den beiden letzten mit „Erneut versuchen“, während des Sendens deaktiviert). Die Adresse wird nicht an die Folgeseite übergeben. Keys: `forgot_password_screen`, `forgot_password_email`, `forgot_password_submit`, `forgot_password_banner`, `forgot_password_body`, `forgot_password_back`. ### EmailLinkScreen (`/auth/confirm`) Zustände: `idle` (Knopf), `submitting`, `success` (nur Bestätigung), `error` (Link ungültig). ``` E-Mail-Adresse bestätigen | Neues Passwort festlegen (Titel je Typ) Ein Klick genügt, um fortzufahren. ( E-Mail-Adresse bestätigen ) | ( Weiter zum neuen Passwort ) Erfolg: ! Info-Banner "E-Mail bestätigt" (+ Mobile-Web: "Du kannst dich jetzt auch in der App anmelden.") Fehler: ! Fehler-Banner "Der Link ist abgelaufen ..." ( Neuen Link anfordern ) ``` Typ Recovery (01c): Titel, Text und Knopf aus `authRecovery*`; das Einlösen passiert erst per Klick. Bei Erfolg zeigt die Seite nichts weiter an, der Statuswechsel `passwordRecovery` leitet nach `/reset-password`. Ungültiger oder abgelaufener Link (`otp_expired`) → Fehlerzustand mit „Neuen Link anfordern“ → `/forgot-password` (Bestätigung: `/login`). ### ResetPasswordScreen (`/reset-password`) ``` Neues Passwort festlegen Neues Passwort [ ••••••••• (Auge) ] Live-Regeln: authPasswordRule, Fehler je Regel / samePassword ( Passwort speichern ) -> signOut -> /login Erfolg (Ansicht ersetzt das Formular, kein Abbrechen mehr): Passwort geändert. (Hinweis authPasswordChanged) ( Weiter ) -> / ``` Umsetzungsentscheidungen (01c): - **Der Redirect nach `userUpdated` wäre ohne Eingriff ein Fehler:** gotrue liefert `userUpdated` asynchron (auch nach der Rückkehr von `updateUser`); die Regel „angemeldet auf `/reset-password` → `/`“ hätte die Seite entfernt, bevor die Erfolgsmeldung (Krit. 9) sichtbar ist. Lösung: `userUpdated` während der Recovery beendet den Guard **nicht**, sondern setzt `AuthStatus.passwordRecovery(passwordChanged: true)`. Das Repository markiert die Änderung nach erfolgreichem `updateUser` zusätzlich selbst (idempotent), das Formular bleibt bis zur Erfolgsansicht gesperrt. Erst „Weiter“ (`AuthRepository.finishPasswordRecovery()`, vor dem Wechsel ohne Wirkung) beendet den Guard; der Fokus liegt beim Wechsel auf „Weiter“; der Router leitet dann nach `/`. Die Erfolgsansicht leitet sich aus dem Status ab (`passwordRecoveryCompleted`), überlebt also einen Neuaufbau der Seite. Bewusst **kein** zeitgesteuerter Sprung (Screenreader, Lesezeit). Spec § 5.5 ist entsprechend ergänzt. - „Abbrechen“ gibt es nur im Formular, nicht in der Erfolgsansicht (es würde eine gültige Sitzung abmelden). - Passwortregel wie bei der Registrierung (10 Zeichen, Liste häufiger Passwörter; die Liste wird vor dem Absenden geladen, Fallback nur Länge). `weak_password` und `same_password` stehen am Feld, alles andere im Banner. - Hinweistext ehrlich zu F1: andere Geräte können bis zu etwa einer Stunde angemeldet bleiben; nichts von „sofort“. - Keys: `reset_password_screen`, `reset_password_password`, `reset_password_submit`, `reset_password_cancel`, `reset_password_banner`, `reset_password_success_banner`, `reset_password_continue`. ## 3. Textvorschläge Ton: du-Form, kurz, sachlich-freundlich, keine Schuldzuweisung. Platzhalter in ICU-Syntax (`{n}`). Neutrale Texte (Krit. 4, F12) verraten nie, ob eine Adresse registriert ist. ### 3.1 Meldungen aus § 4.4 (Fehlerabbildung) | Schlüssel | de | en | |---|---|---| | `authErrorInvalidCredentials` | E-Mail-Adresse oder Passwort ist nicht korrekt. | Email address or password is incorrect. | | `authErrorEmailNotConfirmed` | Bestätige zuerst deine E-Mail-Adresse. Wir haben dir dazu eine E-Mail geschickt. | Please confirm your email address first. We sent you an email for this. | | `authActionResendConfirmation` | Bestätigungs-E-Mail erneut senden | Resend confirmation email | | `authErrorRateLimited` | Zu viele Versuche. Bitte warte einige Minuten und versuche es erneut. | Too many attempts. Please wait a few minutes and try again. | | `authErrorWeakPasswordLength` | Das Passwort ist zu kurz. | The password is too short. | | `authErrorWeakPasswordCharacters` | Das Passwort ist zu einfach. Wähle ein anderes. | The password is too simple. Choose a different one. | | `authErrorWeakPasswordPwned` | Dieses Passwort ist sehr verbreitet und leicht zu erraten. Wähle ein anderes. | This password is very common and easy to guess. Choose a different one. | | `authErrorSamePassword` | Wähle ein anderes als dein bisheriges Passwort. | Choose a password different from your current one. | | `authErrorLinkInvalidOrExpired` | Der Link ist abgelaufen oder wurde schon benutzt. | The link has expired or has already been used. | | `authActionRequestNewLink` | Neuen Link anfordern | Request a new link | | `authErrorEmailInvalid` | Gib eine gültige E-Mail-Adresse ein. | Enter a valid email address. | | `authErrorRegistrationRejected` | Registrierung nicht möglich. Bitte prüfe deine Angaben. | Registration is not possible. Please check your details. | | `authErrorNetwork` | Keine Verbindung. Für diesen Schritt brauchst du Internet. | No connection. You need internet access for this step. | | `authErrorUnexpected` | Etwas ist schiefgelaufen. Bitte versuche es erneut. | Something went wrong. Please try again. | | `authActionRetry` | Erneut versuchen | Try again | `emailSendLimited` hat bewusst **keinen** eigenen Text: Register/Forgot/CheckEmail zeigen den neutralen Erfolgstext (§ 3.3), nur der Resend im LoginScreen zeigt `authErrorRateLimited`. ### 3.2 Meldungen aus § 5.4 (Felder, Banner, Hinweise) | Schlüssel | de | en | |---|---|---| | `authPasswordTooShort` | Mindestens 10 Zeichen – {missing, plural, =1{es fehlt noch 1 Zeichen} other{es fehlen noch {missing} Zeichen}}. | At least 10 characters – {missing, plural, =1{1 more character needed} other{{missing} more characters needed}}. | | `authPasswordCommon` | Dieses Passwort ist sehr verbreitet und leicht zu erraten. Wähle ein anderes. | This password is very common and easy to guess. Choose a different one. | | `authPasswordRule` | Mindestens 10 Zeichen. Kein sehr verbreitetes Passwort. | At least 10 characters. Not a very common password. | | `authPasswordRequired` | Gib ein Passwort ein. | Enter a password. | | `authDisplayNameEmpty` | Gib einen Anzeigenamen ein. | Enter a display name. | | `authDisplayNameTooLong` | Der Anzeigename darf höchstens {max} Zeichen lang sein. | The display name can be at most {max} characters long. | | `authDisplayNameInvalid` | Dieser Anzeigename ist nicht erlaubt. Verwende sichtbare Zeichen. | This display name is not allowed. Use visible characters. | | `authAgeNotConfirmed` | Shuffle ist erst ab 16 Jahren nutzbar. | Shuffle can only be used from age 16. | | `authTermsNotAccepted` | Bitte stimme Datenschutzerklärung und Nutzungsbedingungen zu. | Please agree to the privacy policy and the terms of use. | | `authFieldEmailLabel` | E-Mail-Adresse | Email address | | `authFieldPasswordLabel` | Passwort | Password | | `authFieldNewPasswordLabel` | Neues Passwort | New password | | `authFieldDisplayNameLabel` | Anzeigename | Display name | | `authPasswordShow` | Passwort anzeigen | Show password | | `authPasswordHide` | Passwort verbergen | Hide password | | `authCheckboxAgeLabel` | Ich bin mindestens 16 Jahre alt | I am at least 16 years old | | `authCheckboxTermsPrefix` | Ich habe die | I have read the | | `authCheckboxTermsPrivacyLink` | Datenschutzerklärung | privacy policy | | `authCheckboxTermsMiddle` | gelesen und stimme den | and agree to the | | `authCheckboxTermsTermsLink` | Nutzungsbedingungen | terms of use | | `authCheckboxTermsSuffix` | zu | (leer) | | `authBannerTypeError` | Fehler | Error | | `authBannerTypeInfo` | Hinweis | Note | | `authSubmitting` | Bitte warten … | Please wait … | Hinweis zur Checkbox „Nutzungsbedingungen“: Satzbau ist je Sprache verschieden. Der Satz wird deshalb aus den Segmenten `Prefix`, `PrivacyLink`, `Middle`, `TermsLink`, `Suffix` zusammengesetzt (`AppCheckboxField.rich`); im Englischen bleibt `Suffix` leer. Alternativ ein ICU-Satz mit Platzhaltern, den der Screen an den Linkstellen teilt – Entscheidung beim flutter-dev (A2). ### 3.3 Screen-Texte | Schlüssel | de | en | |---|---|---| | `authRegisterTitle` | Konto erstellen | Create account | | `authRegisterSubmit` | Konto erstellen | Create account | | `authRegisterHaveAccount` | Schon ein Konto? Anmelden | Already have an account? Sign in | | `authCheckEmailTitle` | Prüfe dein Postfach | Check your inbox | | `authCheckEmailBody` | Wir haben dir eine E-Mail geschickt, falls die Adresse noch nicht registriert ist. Folge dem Link darin, um fortzufahren. | We sent you an email if the address is not registered yet. Follow the link in it to continue. | | `authCheckEmailBodyReset` | Wir haben dir eine E-Mail mit einem Link zum Zurücksetzen geschickt, falls zu dieser Adresse ein Konto besteht. | We sent you an email with a reset link if an account exists for this address. | | `authCheckEmailDelayHint` | Die E-Mail kann einige Minuten dauern. Schau auch im Spam-Ordner nach. | The email can take a few minutes. Also check your spam folder. | | `authCheckEmailValidity` | Der Link ist {duration} gültig. | The link is valid for {duration}. | | `authCheckEmailValidityOneHour` | 1 Stunde | 1 hour | | `authCheckEmailResend` | E-Mail erneut senden | Resend email | | `authCheckEmailResent` | Wir haben die E-Mail erneut gesendet, falls es zu dieser Adresse etwas zu senden gibt. | We resent the email, if there is something to send for this address. | | `authCheckEmailDifferentAddress` | Andere E-Mail-Adresse eingeben | Enter a different email address | | `authBackToLogin` | Zurück zur Anmeldung | Back to sign in | | `authLoginTitle` | Anmelden | Sign in | | `authLoginSubmit` | Anmelden | Sign in | | `authLoginForgotPassword` | Passwort vergessen? | Forgot password? | | `authLoginCreateAccount` | Konto erstellen | Create account | | `authForgotTitle` | Passwort zurücksetzen | Reset password | | `authForgotBody` | Gib die E-Mail-Adresse deines Kontos ein. Wir senden dir einen Link zum Zurücksetzen. | Enter the email address of your account. We will send you a reset link. | | `authForgotSubmit` | Link senden | Send link | | `authConfirmTitle` | E-Mail-Adresse bestätigen | Confirm email address | | `authConfirmBody` | Ein Klick genügt, um deine E-Mail-Adresse zu bestätigen. | One click is enough to confirm your email address. | | `authConfirmSubmit` | E-Mail-Adresse bestätigen | Confirm email address | | `authConfirmSuccess` | E-Mail bestätigt. | Email confirmed. | | `authConfirmSuccessMobileWeb` | Du kannst dich jetzt auch in der App anmelden. | You can now also sign in in the app. | | `authRecoveryTitle` | Neues Passwort festlegen | Set a new password | | `authRecoveryBody` | Ein Klick genügt, um ein neues Passwort festzulegen. | One click is enough to set a new password. | | `authRecoverySubmit` | Weiter zum neuen Passwort | Continue to new password | | `authResetTitle` | Neues Passwort festlegen | Set a new password | | `authResetSubmit` | Passwort speichern | Save password | | `authResetCancel` | Abbrechen | Cancel | | `authPasswordChanged` | Passwort geändert. Andere Geräte werden abgemeldet; das kann bis zu einer Stunde dauern. | Password changed. Other devices will be signed out; this can take up to an hour. | ## 4. Barrierefreiheit pro Screen (Checkliste für A2–A4 und T2) - Jedes Feld hat ein sichtbares Label (kein Platzhalter-Label); Fehler per `errorText` (Icon + Text + Live-Region). - Banner und Fehler werden beim Erscheinen vorgelesen (Live-Region); keine reinen Farb-Signale. - Passwortfelder: `AppPasswordField` mit `isNewPassword: true` bei Register/Reset, sonst `false`; E-Mail mit `AutofillHints.email` (Login: `AutofillHints.username`/`email`), `TextInputType.emailAddress`. - Links in Checkbox-Sätzen (`AppCheckboxField.rich`) sind eigene Semantics-Knoten (`link: true`, Label = Linktext, Tap), keine Custom Actions (Flutter-Web setzt diese nicht in ARIA um). Der Checkbox-Knoten trägt den ganzen Satz. - Meldungen, die sich bei jedem Tastendruck ändern („es fehlen noch n“), sind während des Tippens keine Live-Region (`liveError: false`) und werden beim Verlassen des Feldes angesagt; der sichtbare Text bleibt. - Alle interaktiven Elemente >= 48 dp, Tab-Reihenfolge wie Lesereihenfolge, Fokus sichtbar. - Test je Screen mit `textScaler` 2.0 (Android 320 x 640), `androidTapTargetGuideline`, `labeledTapTargetGuideline`, `textContrastGuideline`, Hell und Dunkel (T2).