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>
84 lines
3.1 KiB
Dart
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;
|