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({}) Set 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._(); 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; }