Shuffle/app/lib/features/auth/presentation/login/login_state.dart
cschlaefke 57305dad4b fix(auth): address review findings for login
Fixed-wording tests for the generic credentials error, redacted toString for
login/register state, shared AuthFailure.isRetryable, autofill context
finished even when the router removes the screen, stricter from validation
(encoded and control-character variants), more widget tests.

Refs #1

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

75 lines
2.3 KiB
Dart

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:shuffle/features/auth/domain/auth_failure.dart';
import 'package:shuffle/features/auth/domain/validation/email_format.dart';
part 'login_state.freezed.dart';
/// Form fields of the sign-in, in reading order.
enum LoginField { email, password }
enum LoginStatus { editing, submitting }
/// Values, visibility of field errors and submit state of the sign-in form.
///
/// Credential failures are never attached to a field (Kriterium 7): they
/// show as [bannerFailure] above the form.
@freezed
abstract class LoginState with _$LoginState {
const factory LoginState({
@Default('') String email,
@Default('') String password,
@Default(<LoginField>{}) Set<LoginField> visibleFields,
@Default(LoginStatus.editing) LoginStatus status,
/// Failure of the last attempt, shown as banner.
AuthFailure? bannerFailure,
/// Address of the last attempt that ended in `emailNotConfirmed`. The
/// resend action uses this one, not whatever is in the field now.
String? unconfirmedEmail,
/// Field that should receive focus after a failed submit.
LoginField? focusField,
/// Incremented whenever [focusField] is set.
@Default(0) int focusRequest,
}) = _LoginState;
const LoginState._();
/// No credentials in logs or error reports.
@override
String toString() =>
'LoginState(email: <redacted>, password: <redacted>, status: $status, '
'bannerFailure: $bannerFailure)';
bool get isSubmitting => status == LoginStatus.submitting;
EmailError? get emailError => EmailFormat.validate(email);
bool get passwordMissing => password.isEmpty;
bool hasError(LoginField field) => switch (field) {
LoginField.email => emailError != null,
LoginField.password => passwordMissing,
};
LoginField? get firstInvalidField {
for (final field in LoginField.values) {
if (hasError(field)) {
return field;
}
}
return null;
}
bool isVisible(LoginField field) => visibleFields.contains(field);
bool get shownEmailError => isVisible(LoginField.email) && emailError != null;
bool get shownPasswordRequired =>
isVisible(LoginField.password) && passwordMissing;
bool get showsNotConfirmed =>
bannerFailure is EmailNotConfirmed && unconfirmedEmail != null;
}