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>
122 lines
4 KiB
Dart
122 lines
4 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/common_passwords.dart';
|
|
import 'package:shuffle/features/auth/domain/validation/display_name_rule.dart';
|
|
import 'package:shuffle/features/auth/domain/validation/email_format.dart';
|
|
import 'package:shuffle/features/auth/domain/validation/password_policy.dart';
|
|
|
|
part 'register_state.freezed.dart';
|
|
|
|
/// Form fields of the registration, in reading order.
|
|
enum RegisterField {
|
|
email,
|
|
password,
|
|
displayName,
|
|
ageConfirmation,
|
|
termsAcceptance,
|
|
}
|
|
|
|
enum RegisterStatus { editing, submitting }
|
|
|
|
/// Values, visibility of errors and submit state of the registration form.
|
|
///
|
|
/// Errors are derived from the values by the domain rules. Which of them are
|
|
/// shown is decided by [visibleFields] (blur, typing in the password field,
|
|
/// submit attempt).
|
|
@freezed
|
|
abstract class RegisterState with _$RegisterState {
|
|
const factory RegisterState({
|
|
@Default('') String email,
|
|
@Default('') String password,
|
|
@Default('') String displayName,
|
|
@Default(false) bool ageConfirmed,
|
|
@Default(false) bool termsAccepted,
|
|
@Default(<RegisterField>{}) Set<RegisterField> visibleFields,
|
|
@Default(RegisterStatus.editing) RegisterStatus status,
|
|
|
|
/// Loaded lazily with the first use of the password field.
|
|
CommonPasswordList? commonPasswords,
|
|
|
|
/// General failure shown as banner.
|
|
AuthFailure? bannerFailure,
|
|
|
|
/// `weak_password` reasons reported by the server, shown at the password.
|
|
List<WeakPasswordReason>? serverPasswordReasons,
|
|
|
|
/// The server rejected the address syntax; shown at the email field.
|
|
@Default(false) bool serverEmailInvalid,
|
|
|
|
/// Field that should receive focus after a failed submit.
|
|
RegisterField? focusField,
|
|
|
|
/// Incremented whenever [focusField] is set, so the screen can react
|
|
/// to repeated requests for the same field.
|
|
@Default(0) int focusRequest,
|
|
}) = _RegisterState;
|
|
|
|
const RegisterState._();
|
|
|
|
/// No credentials or personal data in logs or error reports.
|
|
@override
|
|
String toString() =>
|
|
'RegisterState(email: <redacted>, password: <redacted>, '
|
|
'displayName: <redacted>, status: $status, '
|
|
'bannerFailure: $bannerFailure)';
|
|
|
|
bool get isSubmitting => status == RegisterStatus.submitting;
|
|
|
|
EmailError? get emailError => EmailFormat.validate(email);
|
|
|
|
PasswordError? get passwordError =>
|
|
PasswordPolicy.validate(password, common: commonPasswords);
|
|
|
|
DisplayNameError? get displayNameError =>
|
|
DisplayNameRule.validate(displayName);
|
|
|
|
int get displayNameLength => DisplayNameRule.length(displayName);
|
|
|
|
bool get ageError => !ageConfirmed;
|
|
|
|
bool get termsError => !termsAccepted;
|
|
|
|
bool isVisible(RegisterField field) => visibleFields.contains(field);
|
|
|
|
/// Local error of [field] (ignoring visibility).
|
|
bool hasError(RegisterField field) => switch (field) {
|
|
RegisterField.email => emailError != null,
|
|
RegisterField.password => password.isEmpty || passwordError != null,
|
|
RegisterField.displayName => displayNameError != null,
|
|
RegisterField.ageConfirmation => ageError,
|
|
RegisterField.termsAcceptance => termsError,
|
|
};
|
|
|
|
RegisterField? get firstInvalidField {
|
|
for (final field in RegisterField.values) {
|
|
if (hasError(field)) {
|
|
return field;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Errors as the screen shows them.
|
|
|
|
EmailError? get shownEmailError =>
|
|
isVisible(RegisterField.email) ? emailError : null;
|
|
|
|
/// Empty password is "required", otherwise the policy error.
|
|
bool get shownPasswordRequired =>
|
|
isVisible(RegisterField.password) && password.isEmpty;
|
|
|
|
PasswordError? get shownPasswordError =>
|
|
isVisible(RegisterField.password) ? passwordError : null;
|
|
|
|
DisplayNameError? get shownDisplayNameError =>
|
|
isVisible(RegisterField.displayName) ? displayNameError : null;
|
|
|
|
bool get shownAgeError =>
|
|
isVisible(RegisterField.ageConfirmation) && ageError;
|
|
|
|
bool get shownTermsError =>
|
|
isVisible(RegisterField.termsAcceptance) && termsError;
|
|
}
|