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>
75 lines
2.3 KiB
Dart
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;
|
|
}
|