Bladeren bron

refactor sign in screen according to the ui

appflowy 3 jaren geleden
bovenliggende
commit
771162e80b

+ 1 - 1
app_flowy/lib/startup/deps_inject/prelude.dart

@@ -15,7 +15,7 @@ Future<void> initGetIt(
   getIt.registerLazySingleton<FlowySDK>(() => const FlowySDK());
   getIt.registerLazySingleton<AppLauncher>(() => AppLauncher(env, getIt));
 
-  await WelcomeDepsResolver.resolve(getIt);
   await UserDepsResolver.resolve(getIt);
+  await WelcomeDepsResolver.resolve(getIt);
   await HomeDepsResolver.resolve(getIt);
 }

+ 28 - 7
app_flowy/lib/user/application/sign_in/sign_in_bloc.dart

@@ -22,10 +22,10 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
         );
       },
       emailChanged: (EmailChanged value) async* {
-        yield state.copyWith(email: value.email, signInFailure: none());
+        yield state.copyWith(email: value.email, successOrFail: none());
       },
       passwordChanged: (PasswordChanged value) async* {
-        yield state.copyWith(password: value.password, signInFailure: none());
+        yield state.copyWith(password: value.password, successOrFail: none());
       },
     );
   }
@@ -36,17 +36,34 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
     final result = await authImpl.signIn(state.email, state.password);
     yield result.fold(
       (userDetail) => state.copyWith(
-          isSubmitting: false, signInFailure: some(left(userDetail))),
-      (s) => state.copyWith(isSubmitting: false, signInFailure: some(right(s))),
+          isSubmitting: false, successOrFail: some(left(userDetail))),
+      (error) => stateFromCode(error),
     );
   }
+
+  SignInState stateFromCode(UserError error) {
+    switch (error.code) {
+      case UserErrCode.EmailInvalid:
+        return state.copyWith(
+            isSubmitting: false,
+            emailError: some(error.msg),
+            passwordError: none());
+      case UserErrCode.PasswordInvalid:
+        return state.copyWith(
+            isSubmitting: false,
+            passwordError: some(error.msg),
+            emailError: none());
+      default:
+        return state.copyWith(
+            isSubmitting: false, successOrFail: some(right(error)));
+    }
+  }
 }
 
 @freezed
 abstract class SignInEvent with _$SignInEvent {
   const factory SignInEvent.signedInWithUserEmailAndPassword() =
       SignedInWithUserEmailAndPassword;
-
   const factory SignInEvent.emailChanged(String email) = EmailChanged;
   const factory SignInEvent.passwordChanged(String password) = PasswordChanged;
 }
@@ -57,11 +74,15 @@ abstract class SignInState with _$SignInState {
     String? email,
     String? password,
     required bool isSubmitting,
-    required Option<Either<UserDetail, UserError>> signInFailure,
+    required Option<String> passwordError,
+    required Option<String> emailError,
+    required Option<Either<UserDetail, UserError>> successOrFail,
   }) = _SignInState;
 
   factory SignInState.initial() => SignInState(
         isSubmitting: false,
-        signInFailure: none(),
+        passwordError: none(),
+        emailError: none(),
+        successOrFail: none(),
       );
 }

+ 71 - 21
app_flowy/lib/user/application/sign_in/sign_in_bloc.freezed.dart

@@ -438,12 +438,16 @@ class _$SignInStateTearOff {
       {String? email,
       String? password,
       required bool isSubmitting,
-      required Option<Either<UserDetail, UserError>> signInFailure}) {
+      required Option<String> passwordError,
+      required Option<String> emailError,
+      required Option<Either<UserDetail, UserError>> successOrFail}) {
     return _SignInState(
       email: email,
       password: password,
       isSubmitting: isSubmitting,
-      signInFailure: signInFailure,
+      passwordError: passwordError,
+      emailError: emailError,
+      successOrFail: successOrFail,
     );
   }
 }
@@ -456,7 +460,9 @@ mixin _$SignInState {
   String? get email => throw _privateConstructorUsedError;
   String? get password => throw _privateConstructorUsedError;
   bool get isSubmitting => throw _privateConstructorUsedError;
-  Option<Either<UserDetail, UserError>> get signInFailure =>
+  Option<String> get passwordError => throw _privateConstructorUsedError;
+  Option<String> get emailError => throw _privateConstructorUsedError;
+  Option<Either<UserDetail, UserError>> get successOrFail =>
       throw _privateConstructorUsedError;
 
   @JsonKey(ignore: true)
@@ -473,7 +479,9 @@ abstract class $SignInStateCopyWith<$Res> {
       {String? email,
       String? password,
       bool isSubmitting,
-      Option<Either<UserDetail, UserError>> signInFailure});
+      Option<String> passwordError,
+      Option<String> emailError,
+      Option<Either<UserDetail, UserError>> successOrFail});
 }
 
 /// @nodoc
@@ -489,7 +497,9 @@ class _$SignInStateCopyWithImpl<$Res> implements $SignInStateCopyWith<$Res> {
     Object? email = freezed,
     Object? password = freezed,
     Object? isSubmitting = freezed,
-    Object? signInFailure = freezed,
+    Object? passwordError = freezed,
+    Object? emailError = freezed,
+    Object? successOrFail = freezed,
   }) {
     return _then(_value.copyWith(
       email: email == freezed
@@ -504,9 +514,17 @@ class _$SignInStateCopyWithImpl<$Res> implements $SignInStateCopyWith<$Res> {
           ? _value.isSubmitting
           : isSubmitting // ignore: cast_nullable_to_non_nullable
               as bool,
-      signInFailure: signInFailure == freezed
-          ? _value.signInFailure
-          : signInFailure // ignore: cast_nullable_to_non_nullable
+      passwordError: passwordError == freezed
+          ? _value.passwordError
+          : passwordError // ignore: cast_nullable_to_non_nullable
+              as Option<String>,
+      emailError: emailError == freezed
+          ? _value.emailError
+          : emailError // ignore: cast_nullable_to_non_nullable
+              as Option<String>,
+      successOrFail: successOrFail == freezed
+          ? _value.successOrFail
+          : successOrFail // ignore: cast_nullable_to_non_nullable
               as Option<Either<UserDetail, UserError>>,
     ));
   }
@@ -523,7 +541,9 @@ abstract class _$SignInStateCopyWith<$Res>
       {String? email,
       String? password,
       bool isSubmitting,
-      Option<Either<UserDetail, UserError>> signInFailure});
+      Option<String> passwordError,
+      Option<String> emailError,
+      Option<Either<UserDetail, UserError>> successOrFail});
 }
 
 /// @nodoc
@@ -541,7 +561,9 @@ class __$SignInStateCopyWithImpl<$Res> extends _$SignInStateCopyWithImpl<$Res>
     Object? email = freezed,
     Object? password = freezed,
     Object? isSubmitting = freezed,
-    Object? signInFailure = freezed,
+    Object? passwordError = freezed,
+    Object? emailError = freezed,
+    Object? successOrFail = freezed,
   }) {
     return _then(_SignInState(
       email: email == freezed
@@ -556,9 +578,17 @@ class __$SignInStateCopyWithImpl<$Res> extends _$SignInStateCopyWithImpl<$Res>
           ? _value.isSubmitting
           : isSubmitting // ignore: cast_nullable_to_non_nullable
               as bool,
-      signInFailure: signInFailure == freezed
-          ? _value.signInFailure
-          : signInFailure // ignore: cast_nullable_to_non_nullable
+      passwordError: passwordError == freezed
+          ? _value.passwordError
+          : passwordError // ignore: cast_nullable_to_non_nullable
+              as Option<String>,
+      emailError: emailError == freezed
+          ? _value.emailError
+          : emailError // ignore: cast_nullable_to_non_nullable
+              as Option<String>,
+      successOrFail: successOrFail == freezed
+          ? _value.successOrFail
+          : successOrFail // ignore: cast_nullable_to_non_nullable
               as Option<Either<UserDetail, UserError>>,
     ));
   }
@@ -571,7 +601,9 @@ class _$_SignInState implements _SignInState {
       {this.email,
       this.password,
       required this.isSubmitting,
-      required this.signInFailure});
+      required this.passwordError,
+      required this.emailError,
+      required this.successOrFail});
 
   @override
   final String? email;
@@ -580,11 +612,15 @@ class _$_SignInState implements _SignInState {
   @override
   final bool isSubmitting;
   @override
-  final Option<Either<UserDetail, UserError>> signInFailure;
+  final Option<String> passwordError;
+  @override
+  final Option<String> emailError;
+  @override
+  final Option<Either<UserDetail, UserError>> successOrFail;
 
   @override
   String toString() {
-    return 'SignInState(email: $email, password: $password, isSubmitting: $isSubmitting, signInFailure: $signInFailure)';
+    return 'SignInState(email: $email, password: $password, isSubmitting: $isSubmitting, passwordError: $passwordError, emailError: $emailError, successOrFail: $successOrFail)';
   }
 
   @override
@@ -599,9 +635,15 @@ class _$_SignInState implements _SignInState {
             (identical(other.isSubmitting, isSubmitting) ||
                 const DeepCollectionEquality()
                     .equals(other.isSubmitting, isSubmitting)) &&
-            (identical(other.signInFailure, signInFailure) ||
+            (identical(other.passwordError, passwordError) ||
+                const DeepCollectionEquality()
+                    .equals(other.passwordError, passwordError)) &&
+            (identical(other.emailError, emailError) ||
                 const DeepCollectionEquality()
-                    .equals(other.signInFailure, signInFailure)));
+                    .equals(other.emailError, emailError)) &&
+            (identical(other.successOrFail, successOrFail) ||
+                const DeepCollectionEquality()
+                    .equals(other.successOrFail, successOrFail)));
   }
 
   @override
@@ -610,7 +652,9 @@ class _$_SignInState implements _SignInState {
       const DeepCollectionEquality().hash(email) ^
       const DeepCollectionEquality().hash(password) ^
       const DeepCollectionEquality().hash(isSubmitting) ^
-      const DeepCollectionEquality().hash(signInFailure);
+      const DeepCollectionEquality().hash(passwordError) ^
+      const DeepCollectionEquality().hash(emailError) ^
+      const DeepCollectionEquality().hash(successOrFail);
 
   @JsonKey(ignore: true)
   @override
@@ -623,7 +667,9 @@ abstract class _SignInState implements SignInState {
           {String? email,
           String? password,
           required bool isSubmitting,
-          required Option<Either<UserDetail, UserError>> signInFailure}) =
+          required Option<String> passwordError,
+          required Option<String> emailError,
+          required Option<Either<UserDetail, UserError>> successOrFail}) =
       _$_SignInState;
 
   @override
@@ -633,7 +679,11 @@ abstract class _SignInState implements SignInState {
   @override
   bool get isSubmitting => throw _privateConstructorUsedError;
   @override
-  Option<Either<UserDetail, UserError>> get signInFailure =>
+  Option<String> get passwordError => throw _privateConstructorUsedError;
+  @override
+  Option<String> get emailError => throw _privateConstructorUsedError;
+  @override
+  Option<Either<UserDetail, UserError>> get successOrFail =>
       throw _privateConstructorUsedError;
   @override
   @JsonKey(ignore: true)

+ 0 - 0
app_flowy/lib/user/application/sign_up/sign_up_event.dart


+ 0 - 1
app_flowy/lib/user/application/sign_up/sign_up_state.dart

@@ -1 +0,0 @@
-

+ 7 - 0
app_flowy/lib/user/domain/i_auth.dart

@@ -1,5 +1,6 @@
 import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart';
 import 'package:dartz/dartz.dart';
+import 'package:flutter/material.dart';
 
 abstract class IAuth {
   Future<Either<UserDetail, UserError>> signIn(String? email, String? password);
@@ -8,3 +9,9 @@ abstract class IAuth {
 
   Future<Either<Unit, UserError>> signOut();
 }
+
+abstract class IAuthRouter {
+  void showHomeScreen(BuildContext context, UserDetail user);
+  void showSignUpScreen(BuildContext context);
+  void showForgetPasswordScreen(BuildContext context);
+}

+ 2 - 1
app_flowy/lib/user/infrastructure/deps_resolver.dart

@@ -6,10 +6,11 @@ import 'package:get_it/get_it.dart';
 
 class UserDepsResolver {
   static Future<void> resolve(GetIt getIt) async {
-    getIt.registerLazySingleton<AuthRepository>(() => AuthRepository());
+    getIt.registerFactory<AuthRepository>(() => AuthRepository());
 
     //Interface implementation
     getIt.registerFactory<IAuth>(() => AuthImpl(repo: getIt<AuthRepository>()));
+    getIt.registerFactory<IAuthRouter>(() => AuthRouterImpl());
 
     //Bloc
     getIt.registerFactory<SignInBloc>(() => SignInBloc(getIt<IAuth>()));

+ 26 - 0
app_flowy/lib/user/infrastructure/i_auth_impl.dart

@@ -1,7 +1,9 @@
+import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart';
 import 'package:app_flowy/user/domain/i_auth.dart';
 import 'package:app_flowy/user/infrastructure/repos/auth_repo.dart';
+import 'package:flutter/material.dart';
 
 class AuthImpl extends IAuth {
   AuthRepository repo;
@@ -26,3 +28,27 @@ class AuthImpl extends IAuth {
     return repo.signOut();
   }
 }
+
+class AuthRouterImpl extends IAuthRouter {
+  @override
+  void showForgetPasswordScreen(BuildContext context) {
+    // TODO: implement showForgetPasswordScreen
+  }
+
+  @override
+  void showHomeScreen(BuildContext context, UserDetail user) {
+    Navigator.pushReplacement(
+      context,
+      MaterialPageRoute(
+        builder: (context) {
+          return HomeScreen(user);
+        },
+      ),
+    );
+  }
+
+  @override
+  void showSignUpScreen(BuildContext context) {
+    // TODO: implement showSignUpScreen
+  }
+}

+ 154 - 83
app_flowy/lib/user/presentation/sign_in/sign_in_screen.dart

@@ -1,7 +1,7 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/user/application/sign_in/sign_in_bloc.dart';
+import 'package:app_flowy/user/domain/i_auth.dart';
 import 'package:app_flowy/user/presentation/sign_in/widgets/background.dart';
-import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
 import 'package:flowy_infra_ui/widget/rounded_button.dart';
 import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
@@ -12,35 +12,32 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:dartz/dartz.dart';
 
 class SignInScreen extends StatelessWidget {
-  const SignInScreen({Key? key}) : super(key: key);
+  final IAuthRouter router;
+  const SignInScreen({Key? key, required this.router}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) => getIt<SignInBloc>(),
-      child: Scaffold(
-        body: BlocProvider(
-          create: (context) => getIt<SignInBloc>(),
-          child: BlocConsumer<SignInBloc, SignInState>(
-            listenWhen: (p, c) => p != c,
-            listener: (context, state) {
-              state.signInFailure.fold(
-                () {},
-                (result) => _handleStateErrors(result, context),
-              );
-            },
-            builder: (context, state) => const SignInForm(),
-          ),
+      child: BlocListener<SignInBloc, SignInState>(
+        listener: (context, state) {
+          state.successOrFail.fold(
+            () => null,
+            (result) => _handleSuccessOrFail(result, context),
+          );
+        },
+        child: Scaffold(
+          body: SignInForm(router: router),
         ),
       ),
     );
   }
 
-  void _handleStateErrors(
-      Either<UserDetail, UserError> some, BuildContext context) {
-    some.fold(
-      (userDetail) => _showHomeScreen(context, userDetail),
-      (result) => _showErrorMessage(context, result.msg),
+  void _handleSuccessOrFail(
+      Either<UserDetail, UserError> result, BuildContext context) {
+    result.fold(
+      (user) => router.showHomeScreen(context, user),
+      (error) => _showErrorMessage(context, error.msg),
     );
   }
 
@@ -51,22 +48,13 @@ class SignInScreen extends StatelessWidget {
       ),
     );
   }
-
-  void _showHomeScreen(BuildContext context, UserDetail userDetail) {
-    Navigator.pushReplacement(
-      context,
-      MaterialPageRoute(
-        builder: (context) {
-          return HomeScreen(userDetail);
-        },
-      ),
-    );
-  }
 }
 
 class SignInForm extends StatelessWidget {
+  final IAuthRouter router;
   const SignInForm({
     Key? key,
+    required this.router,
   }) : super(key: key);
 
   @override
@@ -80,57 +68,12 @@ class SignInForm extends StatelessWidget {
             logoSize: Size(60, 60),
           ),
           const VSpace(30),
-          RoundedInputField(
-            hintText: 'email',
-            onChanged: (value) =>
-                context.read<SignInBloc>().add(SignInEvent.emailChanged(value)),
-          ),
-          RoundedInputField(
-            obscureText: true,
-            hintText: 'password',
-            onChanged: (value) => context
-                .read<SignInBloc>()
-                .add(SignInEvent.passwordChanged(value)),
-          ),
-          TextButton(
-            style: TextButton.styleFrom(
-              textStyle: const TextStyle(fontSize: 12),
-            ),
-            onPressed: () => _showForgetPasswordScreen(context),
-            child: const Text(
-              'Forgot Password?',
-              style: TextStyle(color: Colors.lightBlue),
-            ),
-          ),
-          RoundedButton(
-            title: 'Login',
-            height: 60,
-            borderRadius: BorderRadius.circular(10),
-            color: Colors.lightBlue,
-            press: () {
-              context
-                  .read<SignInBloc>()
-                  .add(const SignInEvent.signedInWithUserEmailAndPassword());
-            },
-          ),
+          const EmailTextField(),
+          const PasswordTextField(),
+          ForgetPasswordButton(router: router),
+          const LoginButton(),
           const VSpace(10),
-          Row(
-            children: [
-              const Text("Dont't have an account",
-                  style: TextStyle(color: Colors.blueGrey, fontSize: 12)),
-              TextButton(
-                style: TextButton.styleFrom(
-                  textStyle: const TextStyle(fontSize: 12),
-                ),
-                onPressed: () {},
-                child: const Text(
-                  'Sign Up',
-                  style: TextStyle(color: Colors.lightBlue),
-                ),
-              ),
-            ],
-            mainAxisAlignment: MainAxisAlignment.center,
-          ),
+          SignUpPrompt(router: router),
           if (context.read<SignInBloc>().state.isSubmitting) ...[
             const SizedBox(height: 8),
             const LinearProgressIndicator(value: null),
@@ -139,8 +82,136 @@ class SignInForm extends StatelessWidget {
       ),
     );
   }
+}
+
+class SignUpPrompt extends StatelessWidget {
+  const SignUpPrompt({
+    Key? key,
+    required this.router,
+  }) : super(key: key);
+
+  final IAuthRouter router;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        const Text("Dont't have an account",
+            style: TextStyle(color: Colors.blueGrey, fontSize: 12)),
+        TextButton(
+          style: TextButton.styleFrom(
+            textStyle: const TextStyle(fontSize: 12),
+          ),
+          onPressed: () => router.showSignUpScreen(context),
+          child: const Text(
+            'Sign Up',
+            style: TextStyle(color: Colors.lightBlue),
+          ),
+        ),
+      ],
+      mainAxisAlignment: MainAxisAlignment.center,
+    );
+  }
+}
+
+class LoginButton extends StatelessWidget {
+  const LoginButton({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return RoundedButton(
+      title: 'Login',
+      height: 65,
+      borderRadius: BorderRadius.circular(10),
+      color: Colors.lightBlue,
+      press: () {
+        context
+            .read<SignInBloc>()
+            .add(const SignInEvent.signedInWithUserEmailAndPassword());
+      },
+    );
+  }
+}
+
+class ForgetPasswordButton extends StatelessWidget {
+  const ForgetPasswordButton({
+    Key? key,
+    required this.router,
+  }) : super(key: key);
+
+  final IAuthRouter router;
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+      style: TextButton.styleFrom(
+        textStyle: const TextStyle(fontSize: 12),
+      ),
+      onPressed: () => router.showForgetPasswordScreen(context),
+      child: const Text(
+        'Forgot Password?',
+        style: TextStyle(color: Colors.lightBlue),
+      ),
+    );
+  }
+}
 
-  void _showForgetPasswordScreen(BuildContext context) {
-    throw UnimplementedError();
+class PasswordTextField extends StatelessWidget {
+  const PasswordTextField({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<SignInBloc, SignInState>(
+      buildWhen: (previous, current) =>
+          previous.passwordError != current.passwordError,
+      builder: (context, state) {
+        return RoundedInputField(
+          obscureText: true,
+          hintText: 'password',
+          normalBorderColor: Colors.green,
+          highlightBorderColor: Colors.red,
+          errorText: context
+              .read<SignInBloc>()
+              .state
+              .passwordError
+              .fold(() => "", (error) => error),
+          onChanged: (value) => context
+              .read<SignInBloc>()
+              .add(SignInEvent.passwordChanged(value)),
+        );
+      },
+    );
+  }
+}
+
+class EmailTextField extends StatelessWidget {
+  const EmailTextField({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<SignInBloc, SignInState>(
+      buildWhen: (previous, current) =>
+          previous.emailError != current.emailError,
+      builder: (context, state) {
+        return RoundedInputField(
+          hintText: 'email',
+          normalBorderColor: Colors.green,
+          highlightBorderColor: Colors.red,
+          errorText: context
+              .read<SignInBloc>()
+              .state
+              .emailError
+              .fold(() => "", (error) => error),
+          onChanged: (value) =>
+              context.read<SignInBloc>().add(SignInEvent.emailChanged(value)),
+        );
+      },
+    );
   }
 }

+ 3 - 1
app_flowy/lib/welcome/infrastructure/i_welcome_impl.dart

@@ -1,3 +1,5 @@
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/user/domain/i_auth.dart';
 import 'package:app_flowy/user/presentation/sign_in/sign_in_screen.dart';
 import 'package:app_flowy/welcome/domain/auth_state.dart';
 import 'package:app_flowy/welcome/domain/i_welcome.dart';
@@ -34,6 +36,6 @@ class WelcomeRoute implements IWelcomeRoute {
 
   @override
   Widget pushSignInScreen() {
-    return const SignInScreen();
+    return SignInScreen(router: getIt<IAuthRouter>());
   }
 }

+ 37 - 11
app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart

@@ -1,10 +1,14 @@
 import 'package:flowy_infra_ui/widget/text_field_container.dart';
 import 'package:flutter/material.dart';
+import 'package:flowy_infra/time/duration.dart';
 
 class RoundedInputField extends StatelessWidget {
   final String? hintText;
   final IconData? icon;
   final bool obscureText;
+  final Color normalBorderColor;
+  final Color highlightBorderColor;
+  final String errorText;
   final ValueChanged<String>? onChanged;
 
   const RoundedInputField({
@@ -13,6 +17,9 @@ class RoundedInputField extends StatelessWidget {
     this.icon,
     this.obscureText = false,
     this.onChanged,
+    this.normalBorderColor = Colors.transparent,
+    this.highlightBorderColor = Colors.transparent,
+    this.errorText = "",
   }) : super(key: key);
 
   @override
@@ -24,19 +31,38 @@ class RoundedInputField extends StatelessWidget {
             color: const Color(0xFF6F35A5),
           );
 
-    return TextFieldContainer(
-      borderRadius: BorderRadius.circular(10),
-      borderColor: Colors.blueGrey,
-      child: TextFormField(
-        onChanged: onChanged,
-        cursorColor: const Color(0xFF6F35A5),
-        obscureText: obscureText,
-        decoration: InputDecoration(
-          icon: newIcon,
-          hintText: hintText,
-          border: InputBorder.none,
+    var borderColor = normalBorderColor;
+    if (errorText.isNotEmpty) {
+      borderColor = highlightBorderColor;
+    }
+
+    List<Widget> children = [
+      TextFieldContainer(
+        borderRadius: BorderRadius.circular(10),
+        borderColor: borderColor,
+        child: TextFormField(
+          onChanged: onChanged,
+          cursorColor: const Color(0xFF6F35A5),
+          obscureText: obscureText,
+          decoration: InputDecoration(
+            icon: newIcon,
+            hintText: hintText,
+            border: InputBorder.none,
+          ),
         ),
       ),
+    ];
+
+    if (errorText.isNotEmpty) {
+      children
+          .add(Text(errorText, style: TextStyle(color: highlightBorderColor)));
+    }
+
+    return AnimatedContainer(
+      duration: .3.seconds,
+      child: Column(
+        children: children,
+      ),
     );
   }
 }