Shuffle/docs/design/auth-screens.md
cschlaefke 87c0d7d1ec feat(auth): add password reset (forgot password, recovery link, new password)
ForgotPasswordScreen with neutral reaction (F12), CheckEmail reset mode on
its own path, recovery link handling and ResetPasswordScreen with the
recovery guard held until the user confirms the success message. Carries
the from parameter between login, register and forgot password. Removes
the route placeholders.

Refs #1

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

19 KiB
Raw Blame History

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 TextButtons (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.

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 <Datenschutzerklärung> gelesen und stimme den <Nutzungsbedingungen> zu
                                              Fehler: authTermsNotAccepted
( Konto erstellen )
<Schon ein Konto? Anmelden>

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
<Zurück zur Anmeldung>     (Modus Reset: statt des Resend-Knopfs <Andere E-Mail-Adresse eingeben> -> /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 )
<Passwort vergessen?>   -> /forgot-password
<Konto erstellen>       -> /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 oder />). 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 )
<Zurück zur Anmeldung>   -> /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 )
<Abbrechen>           -> 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 sendet userUpdated, bevor updateUser zurückkehrt; 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). Erst „Weiter“ (AuthRepository.finishPasswordRecovery()) beendet den Guard; 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. Auf anderen Geräten wirst du spätestens nach etwa einer Stunde abgemeldet. Password changed. On other devices you will be signed out after about an hour at the latest.

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).