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