refactor sign in screen according to the ui

This commit is contained in:
appflowy 2021-07-25 18:04:16 +08:00
parent 084f939ed0
commit 771162e80b
11 changed files with 329 additions and 126 deletions

View File

@ -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);
}

View File

@ -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(),
);
}

View File

@ -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.signInFailure, signInFailure)));
.equals(other.passwordError, passwordError)) &&
(identical(other.emailError, emailError) ||
const DeepCollectionEquality()
.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)

View File

@ -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);
}

View File

@ -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>()));

View File

@ -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
}
}

View File

@ -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 {
),
);
}
}
void _showForgetPasswordScreen(BuildContext context) {
throw UnimplementedError();
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),
),
);
}
}
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)),
);
},
);
}
}

View File

@ -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>());
}
}

View File

@ -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,
),
);
}
}