diff --git a/lib/core/validator/email_is_valid.dart b/lib/core/validator/email_is_valid.dart deleted file mode 100644 index 4399477dd..000000000 --- a/lib/core/validator/email_is_valid.dart +++ /dev/null @@ -1,3 +0,0 @@ -bool emailIsValid(String email) { - return RegExp(r'^[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]{2,}').hasMatch(email); -} diff --git a/lib/features/login/presentation/pages/login_page_email.dart b/lib/features/login/presentation/pages/login_page_email.dart index 1d17b69e2..230c22d7d 100644 --- a/lib/features/login/presentation/pages/login_page_email.dart +++ b/lib/features/login/presentation/pages/login_page_email.dart @@ -1,5 +1,4 @@ import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/email_is_valid.dart'; import 'package:coffeecard/core/widgets/fast_slide_transition.dart'; import 'package:coffeecard/features/login/presentation/pages/login_page_base.dart'; import 'package:coffeecard/features/login/presentation/pages/login_page_passcode.dart'; @@ -8,6 +7,12 @@ import 'package:coffeecard/features/login/presentation/widgets/login_email_text_ import 'package:coffeecard/features/register/presentation/pages/register_flow.dart'; import 'package:flutter/material.dart'; +// FIXME: Duplicate code. +// Rewrite the LoginPageEmail widget to use the Form widget, +// such that we can use input validators. +final _isValidEmail = + RegExp(r'^[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]{2,}').hasMatch; + class LoginPageEmail extends StatefulWidget { const LoginPageEmail({this.transitionDuration = Duration.zero}); @@ -50,7 +55,7 @@ class _LoginPageEmailState extends State void _validateEmail(BuildContext context, String email) { if (email.isEmpty) { error = Strings.loginEnterEmailError; - } else if (!emailIsValid(email)) { + } else if (!_isValidEmail(email)) { error = Strings.loginInvalidEmailError; } else { final _ = diff --git a/lib/features/login/presentation/widgets/forgot_passcode_form.dart b/lib/features/login/presentation/widgets/forgot_passcode_form.dart index fc1d54555..8bbca6c99 100644 --- a/lib/features/login/presentation/widgets/forgot_passcode_form.dart +++ b/lib/features/login/presentation/widgets/forgot_passcode_form.dart @@ -1,10 +1,9 @@ import 'package:coffeecard/core/ignore_value.dart'; import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; import 'package:coffeecard/core/widgets/components/dialog.dart'; import 'package:coffeecard/core/widgets/components/loading_overlay.dart'; -import 'package:coffeecard/features/form/presentation/widgets/form.dart'; import 'package:coffeecard/features/login/data/datasources/account_remote_data_source.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:flutter/material.dart'; import 'package:fpdart/fpdart.dart'; @@ -34,7 +33,7 @@ class ForgotPasscodeForm extends StatelessWidget { return emailExistsResult.fold( (error) => const Left(Strings.emailValidationError), (emailExists) => emailExists - ? const Right(null) + ? const Right(unit) : const Left(Strings.forgotPasscodeNoAccountExists), ); }, diff --git a/lib/features/register/presentation/widgets/forms/register_email_form.dart b/lib/features/register/presentation/widgets/forms/register_email_form.dart index 81d2ea933..aee1eb64c 100644 --- a/lib/features/register/presentation/widgets/forms/register_email_form.dart +++ b/lib/features/register/presentation/widgets/forms/register_email_form.dart @@ -1,7 +1,6 @@ import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; -import 'package:coffeecard/features/form/presentation/widgets/form.dart'; import 'package:coffeecard/features/login/data/datasources/account_remote_data_source.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:flutter/material.dart'; import 'package:fpdart/fpdart.dart'; @@ -30,7 +29,7 @@ class RegisterEmailForm extends StatelessWidget { (l) => const Left(Strings.emailValidationError), (r) => r ? Left(Strings.registerEmailInUse(text)) - : const Right(null), + : const Right(unit), ); }, ), diff --git a/lib/features/register/presentation/widgets/forms/register_name_form.dart b/lib/features/register/presentation/widgets/forms/register_name_form.dart index 548467dc8..533a33d03 100644 --- a/lib/features/register/presentation/widgets/forms/register_name_form.dart +++ b/lib/features/register/presentation/widgets/forms/register_name_form.dart @@ -1,11 +1,10 @@ import 'package:coffeecard/core/ignore_value.dart'; import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; import 'package:coffeecard/core/widgets/components/dialog.dart'; import 'package:coffeecard/core/widgets/components/helpers/unordered_list_builder.dart'; import 'package:coffeecard/core/widgets/components/loading_overlay.dart'; -import 'package:coffeecard/features/form/presentation/widgets/form.dart'; import 'package:coffeecard/features/register/presentation/cubit/register_cubit.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; diff --git a/lib/features/register/presentation/widgets/forms/register_passcode_form.dart b/lib/features/register/presentation/widgets/forms/register_passcode_form.dart index 57441479e..864f3c77b 100644 --- a/lib/features/register/presentation/widgets/forms/register_passcode_form.dart +++ b/lib/features/register/presentation/widgets/forms/register_passcode_form.dart @@ -1,6 +1,5 @@ import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; -import 'package:coffeecard/features/form/presentation/widgets/form.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:flutter/material.dart'; class RegisterPasscodeForm extends StatelessWidget { diff --git a/lib/features/register/presentation/widgets/forms/register_passcode_repeat_form.dart b/lib/features/register/presentation/widgets/forms/register_passcode_repeat_form.dart index 4bc93498c..4877c4134 100644 --- a/lib/features/register/presentation/widgets/forms/register_passcode_repeat_form.dart +++ b/lib/features/register/presentation/widgets/forms/register_passcode_repeat_form.dart @@ -1,6 +1,5 @@ import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; -import 'package:coffeecard/features/form/presentation/widgets/form.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:flutter/material.dart'; class RegisterPasscodeRepeatForm extends StatelessWidget { diff --git a/lib/features/settings/presentation/widgets/forms/change_email_form.dart b/lib/features/settings/presentation/widgets/forms/change_email_form.dart index fb74c120f..5ba321777 100644 --- a/lib/features/settings/presentation/widgets/forms/change_email_form.dart +++ b/lib/features/settings/presentation/widgets/forms/change_email_form.dart @@ -1,7 +1,6 @@ import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; -import 'package:coffeecard/features/form/presentation/widgets/form.dart'; import 'package:coffeecard/features/login/data/datasources/account_remote_data_source.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:flutter/material.dart'; import 'package:fpdart/fpdart.dart'; @@ -37,7 +36,7 @@ class ChangeEmailForm extends StatelessWidget { (l) => const Left(Strings.emailValidationError), (r) => r ? Left(Strings.registerEmailInUse(text)) - : const Right(null), + : const Right(unit), ); }, ), diff --git a/lib/features/settings/presentation/widgets/forms/change_name_form.dart b/lib/features/settings/presentation/widgets/forms/change_name_form.dart index 74856287f..be805db55 100644 --- a/lib/features/settings/presentation/widgets/forms/change_name_form.dart +++ b/lib/features/settings/presentation/widgets/forms/change_name_form.dart @@ -1,6 +1,5 @@ import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; -import 'package:coffeecard/features/form/presentation/widgets/form.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/settings/presentation/widgets/forms/change_passcode_form.dart b/lib/features/settings/presentation/widgets/forms/change_passcode_form.dart index 230e6b5a6..cee4db0d1 100644 --- a/lib/features/settings/presentation/widgets/forms/change_passcode_form.dart +++ b/lib/features/settings/presentation/widgets/forms/change_passcode_form.dart @@ -1,8 +1,7 @@ import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; import 'package:coffeecard/core/widgets/fast_slide_transition.dart'; -import 'package:coffeecard/features/form/presentation/widgets/form.dart'; import 'package:coffeecard/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:flutter/material.dart'; class ChangePasscodeForm extends StatelessWidget { diff --git a/lib/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart b/lib/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart index a00ffbb10..e6e403059 100644 --- a/lib/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart +++ b/lib/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart @@ -1,7 +1,6 @@ import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; import 'package:coffeecard/core/widgets/fast_slide_transition.dart'; -import 'package:coffeecard/features/form/presentation/widgets/form.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/shared/form.dart b/lib/features/shared/form.dart new file mode 100644 index 000000000..572046c00 --- /dev/null +++ b/lib/features/shared/form.dart @@ -0,0 +1 @@ +export 'package:coffeecard/src/shared/form/form.dart'; diff --git a/lib/features/voucher/presentation/widgets/voucher_form.dart b/lib/features/voucher/presentation/widgets/voucher_form.dart index c51588e43..5c55b61bd 100644 --- a/lib/features/voucher/presentation/widgets/voucher_form.dart +++ b/lib/features/voucher/presentation/widgets/voucher_form.dart @@ -1,6 +1,5 @@ import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; -import 'package:coffeecard/features/form/presentation/widgets/form.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:coffeecard/features/voucher/presentation/cubit/voucher_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/core/debouncing.dart b/lib/src/shared/form/bloc/debouncing.dart similarity index 100% rename from lib/core/debouncing.dart rename to lib/src/shared/form/bloc/debouncing.dart diff --git a/lib/features/form/presentation/bloc/form_bloc.dart b/lib/src/shared/form/bloc/form_bloc.dart similarity index 87% rename from lib/features/form/presentation/bloc/form_bloc.dart rename to lib/src/shared/form/bloc/form_bloc.dart index a00647889..e97a53b27 100644 --- a/lib/features/form/presentation/bloc/form_bloc.dart +++ b/lib/src/shared/form/bloc/form_bloc.dart @@ -1,6 +1,5 @@ import 'package:bloc/bloc.dart'; -import 'package:coffeecard/core/debouncing.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:equatable/equatable.dart'; import 'package:fpdart/fpdart.dart'; @@ -25,7 +24,7 @@ class FormBloc extends Bloc { state.copyWith( loading: false, canSubmit: false, - error: either, + validationStatus: either, shouldDisplayError: validator.forceErrorMessage ? true : null, ), ); @@ -37,7 +36,7 @@ class FormBloc extends Bloc { loading: false, text: text, canSubmit: true, - error: const Right(null), + validationStatus: const Right(unit), ), ); }, diff --git a/lib/features/form/presentation/bloc/form_event.dart b/lib/src/shared/form/bloc/form_event.dart similarity index 100% rename from lib/features/form/presentation/bloc/form_event.dart rename to lib/src/shared/form/bloc/form_event.dart diff --git a/lib/features/form/presentation/bloc/form_state.dart b/lib/src/shared/form/bloc/form_state.dart similarity index 77% rename from lib/features/form/presentation/bloc/form_state.dart rename to lib/src/shared/form/bloc/form_state.dart index e7f402821..ab876fd91 100644 --- a/lib/features/form/presentation/bloc/form_state.dart +++ b/lib/src/shared/form/bloc/form_state.dart @@ -6,14 +6,14 @@ class FormState extends Equatable { this.text = '', this.canSubmit = false, this.shouldDisplayError = false, - this.error = const Right(null), + this.validationStatus = const Right(unit), }); final bool loading; final String text; final bool canSubmit; final bool shouldDisplayError; - final ErrorEither error; + final Either validationStatus; @override List get props => [ @@ -21,7 +21,7 @@ class FormState extends Equatable { text, canSubmit, shouldDisplayError, - error, + validationStatus, ]; FormState copyWith({ @@ -29,14 +29,14 @@ class FormState extends Equatable { String? text, bool? canSubmit, bool? shouldDisplayError, - ErrorEither? error, + Either? validationStatus, }) { return FormState( loading: loading ?? this.loading, text: text ?? this.text, canSubmit: canSubmit ?? this.canSubmit, shouldDisplayError: shouldDisplayError ?? this.shouldDisplayError, - error: error ?? this.error, + validationStatus: validationStatus ?? this.validationStatus, ); } } diff --git a/lib/src/shared/form/form.dart b/lib/src/shared/form/form.dart new file mode 100644 index 000000000..c018ab6dd --- /dev/null +++ b/lib/src/shared/form/form.dart @@ -0,0 +1,4 @@ +export 'bloc/debouncing.dart'; +export 'bloc/form_bloc.dart'; +export 'input_validator/input_validator.dart'; +export 'widgets/form.dart'; diff --git a/lib/core/validator/input_validator.dart b/lib/src/shared/form/input_validator/input_validator.dart similarity index 83% rename from lib/core/validator/input_validator.dart rename to lib/src/shared/form/input_validator/input_validator.dart index a0ca860c2..87f92625a 100644 --- a/lib/core/validator/input_validator.dart +++ b/lib/src/shared/form/input_validator/input_validator.dart @@ -1,21 +1,17 @@ import 'dart:async'; -import 'package:coffeecard/core/validator/email_is_valid.dart'; import 'package:fpdart/fpdart.dart'; part 'input_validator_helpers.dart'; -/// Either an input error message (String) or a valid input (void). -typedef ErrorEither = Either; - -/// A content validator for `AppForm`s. +/// A content validator for [FormBase] widgets. /// /// Example: /// ```dart /// class ExampleForm extends StatelessWidget { /// @override /// Widget build(BuildContext context) { -/// return AppForm( +/// return FormBase( /// inputValidators: [ /// InputValidator.bool( /// validate: (input) => input.length == 4, @@ -54,14 +50,14 @@ class InputValidator { }) : this( validate: (String input) async { final validInput = await validate(input); - return validInput ? const Right(null) : Left(errorMessage); + return validInput ? const Right(unit) : Left(errorMessage); }, forceErrorMessage: forceErrorMessage, ); /// The input validator. Either returns an /// error message (left) or success (right). - final FutureOr Function(String input) validate; + final FutureOr> Function(String input) validate; /// If failing this input validator should show an error message, /// even if the user has not pressed submit yet. diff --git a/lib/core/validator/input_validator_helpers.dart b/lib/src/shared/form/input_validator/input_validator_helpers.dart similarity index 84% rename from lib/core/validator/input_validator_helpers.dart rename to lib/src/shared/form/input_validator/input_validator_helpers.dart index bb1397f76..3d7b46350 100644 --- a/lib/core/validator/input_validator_helpers.dart +++ b/lib/src/shared/form/input_validator/input_validator_helpers.dart @@ -11,7 +11,7 @@ class InputValidators { static InputValidator validEmail({required String errorMessage}) { return InputValidator.bool( - validate: emailIsValid, + validate: RegExp(r'^[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]{2,}').hasMatch, errorMessage: errorMessage, ); } diff --git a/lib/features/form/presentation/widgets/form.dart b/lib/src/shared/form/widgets/form.dart similarity index 90% rename from lib/features/form/presentation/widgets/form.dart rename to lib/src/shared/form/widgets/form.dart index efa118668..9877a59bb 100644 --- a/lib/features/form/presentation/widgets/form.dart +++ b/lib/src/shared/form/widgets/form.dart @@ -1,9 +1,8 @@ import 'package:coffeecard/core/strings.dart'; import 'package:coffeecard/core/styles/app_colors.dart'; -import 'package:coffeecard/core/validator/input_validator.dart'; import 'package:coffeecard/core/widgets/components/rounded_button.dart'; import 'package:coffeecard/core/widgets/components/section_title.dart'; -import 'package:coffeecard/features/form/presentation/bloc/form_bloc.dart'; +import 'package:coffeecard/features/shared/form.dart'; import 'package:flutter/material.dart' hide FormState; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -79,7 +78,6 @@ class FormBase extends StatelessWidget { listenWhen: (_, current) => autoSubmitValidInput && current.canSubmit, listener: (_, state) => onSubmit(state.text), builder: (context, state) { - final bloc = context.read(); return Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -93,12 +91,16 @@ class FormBase extends StatelessWidget { inputValidators: inputValidators, onChanged: (input) { if (!state.loading) { - bloc.add(FormValidateRequested()); + context.read().add(FormValidateRequested()); } - bloc.add(FormValidateStarted(input: input)); + context + .read() + .add(FormValidateStarted(input: input)); }, onEditingComplete: () { - bloc.add(FormToggleErrorDisplay(displayError: true)); + context + .read() + .add(FormToggleErrorDisplay(displayError: true)); if (state.canSubmit) { onSubmit(state.text); } diff --git a/lib/features/form/presentation/widgets/form_text_field.dart b/lib/src/shared/form/widgets/form_text_field.dart similarity index 98% rename from lib/features/form/presentation/widgets/form_text_field.dart rename to lib/src/shared/form/widgets/form_text_field.dart index 9a15b8c0d..4373aa258 100644 --- a/lib/features/form/presentation/widgets/form_text_field.dart +++ b/lib/src/shared/form/widgets/form_text_field.dart @@ -94,7 +94,7 @@ class _FormTextFieldState extends State<_FormTextField> { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final maybeError = state.error.fold((l) => l, (r) => null); + final maybeError = state.validationStatus.fold((l) => l, (r) => null); return Container( margin: const EdgeInsets.only(bottom: 12),