Shuffle/app/lib/features/auth/presentation/auth_failure_texts.dart
cschlaefke 7c2f684098
All checks were successful
CI / flutter (pull_request) Successful in 1m13s
CI / web-build (pull_request) Successful in 55s
CI / supabase (pull_request) Successful in 3s
CI / supabase-tests (pull_request) Successful in 30s
fix(auth): address review findings for password reset
Finish the recovery guard only after the password change, mark the change
in the repository so a late userUpdated event is harmless, keep the form
locked after success, carry from through the reset check-email page,
share the password error texts and focus the continue button.

Refs #1

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

84 lines
3.1 KiB
Dart

import 'package:shuffle/core/l10n/l10n.dart';
import 'package:shuffle/features/auth/domain/auth_failure.dart';
import 'package:shuffle/features/auth/domain/validation/password_policy.dart';
/// User-facing text of an [AuthFailure] (Spec #1 § 4.4, texts from
/// `docs/design/auth-screens.md`).
///
/// `emailSendLimited` has no text of its own: screens treat it as success
/// (F12) or convert it to `rateLimited` beforehand.
String authFailureMessage(AppLocalizations l10n, AuthFailure failure) =>
switch (failure) {
InvalidCredentials() => l10n.authErrorInvalidCredentials,
EmailNotConfirmed() => l10n.authErrorEmailNotConfirmed,
EmailSendLimited() || RateLimited() => l10n.authErrorRateLimited,
WeakPassword(:final reasons) => weakPasswordMessage(l10n, reasons),
SamePassword() => l10n.authErrorSamePassword,
LinkInvalidOrExpired() => l10n.authErrorLinkInvalidOrExpired,
InvalidEmail() => l10n.authErrorEmailInvalid,
RegistrationRejected() => l10n.authErrorRegistrationRejected,
NetworkFailure() => l10n.authErrorNetwork,
Unexpected() => l10n.authErrorUnexpected,
};
/// One sentence per distinct reason; an empty list gets the generic text.
String weakPasswordMessage(
AppLocalizations l10n,
List<WeakPasswordReason> reasons,
) {
final texts = <String>{
for (final reason in reasons)
switch (reason) {
WeakPasswordReason.length => l10n.authErrorWeakPasswordLength,
WeakPasswordReason.pwned => l10n.authErrorWeakPasswordPwned,
WeakPasswordReason.characters ||
WeakPasswordReason.unknown => l10n.authErrorWeakPasswordCharacters,
},
};
return texts.isEmpty ? l10n.authErrorWeakPasswordCharacters : texts.join(' ');
}
/// Failures the user can fix by simply trying again (offline, unexpected).
extension AuthFailureRetry on AuthFailure {
bool get isRetryable => this is NetworkFailure || this is Unexpected;
}
/// Text of the password field error, shared by registration and reset.
///
/// Order: server answer, `same_password`, "required", then the local policy
/// [error] (only passed when it should be shown).
String? passwordFieldError(
AppLocalizations l10n, {
required PasswordError? error,
required bool required,
List<WeakPasswordReason>? serverReasons,
bool serverSamePassword = false,
}) {
if (serverReasons != null) {
return weakPasswordMessage(l10n, serverReasons);
}
if (serverSamePassword) {
return l10n.authErrorSamePassword;
}
if (required) {
return l10n.authPasswordRequired;
}
return switch (error) {
PasswordTooShort(:final missing) => l10n.authPasswordTooShort(missing),
PasswordCommon() => l10n.authPasswordCommon,
null => null,
};
}
/// The only message shown is the "n missing" count, which changes with every
/// keystroke: screens announce it only when the field is left.
bool isPasswordLengthHintOnly({
required PasswordError? error,
required bool required,
List<WeakPasswordReason>? serverReasons,
bool serverSamePassword = false,
}) =>
serverReasons == null &&
!serverSamePassword &&
!required &&
error is PasswordTooShort;