diff --git a/assets/images/ic_eye.svg b/assets/images/ic_eye.svg
new file mode 100644
index 000000000..bf34f9665
--- /dev/null
+++ b/assets/images/ic_eye.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/ic_eye_off.svg b/assets/images/ic_eye_off.svg
new file mode 100644
index 000000000..a5d96b8ea
--- /dev/null
+++ b/assets/images/ic_eye_off.svg
@@ -0,0 +1,4 @@
+
diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart
index aad3162b7..4ceb29fc3 100644
--- a/core/lib/presentation/extensions/color_extension.dart
+++ b/core/lib/presentation/extensions/color_extension.dart
@@ -10,9 +10,15 @@ extension AppColor on Color {
static const textFieldTextColor = Color(0xFF7E869B);
static const textFieldLabelColor = Color(0xFF7E869B);
static const textFieldHintColor = Color(0xFF757575);
- static const textFieldBorderColor = Color(0xfff2f1fd);
- static const textFieldFocusedBorderColor = Color(0xFF837DFF);
- static const textFieldErrorBorderColor = Color(0xffFF5858);
+ static const textFieldBorderColor = Color(0xfff2f3f5);
+ static const textFieldFocusedBorderColor = Color(0xFF007AFF);
+ static const loginTextFieldBorderColor = Color(0xFFF2F3F5);
+ static const textFieldErrorBorderColor = Color(0xffE64646);
+ static const loginTextFieldErrorBorder = Color(0xffE64646);
+ static const loginTextFieldFocusedBorder = Color(0xFF007AFF);
+ static const loginTextFieldHintColor = Color(0xff818C99);
+ static const loginTextFieldBackgroundColor = Color(0xFFF2F3F5);
+ static const loginTextFieldBackgroundErrorColor = Color(0xFFFAEBEB);
static const buttonColor = Color(0xFF837DFF);
static const appColor = Color(0xFF3840F7);
static const nameUserColor = Color(0xFF182952);
diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart
index 875527c74..855c58493 100644
--- a/core/lib/presentation/resources/image_paths.dart
+++ b/core/lib/presentation/resources/image_paths.dart
@@ -104,6 +104,8 @@ class ImagePaths {
String get icDeleteDialogIdentity => _getImagePath('ic_delete_dialog_identity.svg');
String get icDeleteDialogFailed => _getImagePath('ic_delete_dialog_failed.svg');
String get icEdit => _getImagePath('ic_edit.svg');
+ String get icEye => _getImagePath('ic_eye.svg');
+ String get icEyeOff => _getImagePath('ic_eye_off.svg');
String _getImagePath(String imageName) {
return AssetsPaths.images + imageName;
diff --git a/core/lib/presentation/utils/responsive_utils.dart b/core/lib/presentation/utils/responsive_utils.dart
index ce7c9fef2..e1724650a 100644
--- a/core/lib/presentation/utils/responsive_utils.dart
+++ b/core/lib/presentation/utils/responsive_utils.dart
@@ -3,6 +3,8 @@ import 'package:get/get.dart';
class ResponsiveUtils {
+ final int heightShortest = 600;
+
final int minDesktopWidth = 1200;
final int minTabletWidth = 600;
final int minTabletLargeWidth = 900;
@@ -51,4 +53,8 @@ class ResponsiveUtils {
isMobile(context) ? _loginTextFieldWidthSmallScreen : _loginTextFieldWidthLargeScreen;
double getWidthLoginButton() => _loginButtonWidth;
+
+ bool isHeightShortest(BuildContext context) {
+ return MediaQuery.of(context).size.shortestSide < 600;
+ }
}
\ No newline at end of file
diff --git a/core/lib/presentation/views/text/input_decoration_builder.dart b/core/lib/presentation/views/text/input_decoration_builder.dart
index 82223eb69..1b9846262 100644
--- a/core/lib/presentation/views/text/input_decoration_builder.dart
+++ b/core/lib/presentation/views/text/input_decoration_builder.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
abstract class InputDecorationBuilder {
String? prefixText;
+ TextStyle? prefixStyle;
String? labelText;
TextStyle? labelStyle;
String? hintText;
@@ -9,38 +10,43 @@ abstract class InputDecorationBuilder {
EdgeInsets? contentPadding;
OutlineInputBorder? enabledBorder;
OutlineInputBorder? errorBorder;
+ OutlineInputBorder? focusBorder;
String? errorText;
TextStyle? errorTextStyle;
- void setPrefixText(String newPrefixText) {
+ void setPrefixText(String? newPrefixText) {
prefixText = newPrefixText;
}
- void setLabelText(String newLabelText) {
+ void setPrefixStyle(TextStyle? newPrefixStyle) {
+ prefixStyle = newPrefixStyle;
+ }
+
+ void setLabelText(String? newLabelText) {
labelText = newLabelText;
}
- void setLabelStyle(TextStyle newLabelStyle) {
+ void setLabelStyle(TextStyle? newLabelStyle) {
labelStyle = newLabelStyle;
}
- void setHintText(String newHintText) {
+ void setHintText(String? newHintText) {
hintText = newHintText;
}
- void setHintStyle(TextStyle newHintStyle) {
+ void setHintStyle(TextStyle? newHintStyle) {
hintStyle = newHintStyle;
}
- void setContentPadding(EdgeInsets newContentPadding) {
+ void setContentPadding(EdgeInsets? newContentPadding) {
contentPadding = newContentPadding;
}
- void setEnabledBorder(OutlineInputBorder newEnabledBorder) {
+ void setEnabledBorder(OutlineInputBorder? newEnabledBorder) {
enabledBorder = newEnabledBorder;
}
- void setErrorBorder(OutlineInputBorder newErrorBorder) {
+ void setErrorBorder(OutlineInputBorder? newErrorBorder) {
errorBorder = newErrorBorder;
}
@@ -48,10 +54,14 @@ abstract class InputDecorationBuilder {
errorText = newText;
}
- void setErrorTextStyle(TextStyle newStyle) {
+ void setErrorTextStyle(TextStyle? newStyle) {
errorTextStyle = newStyle;
}
+ void setFocusBorder(OutlineInputBorder focusBorder) {
+ focusBorder = focusBorder;
+ }
+
InputDecoration build() {
return InputDecoration(
prefixText: prefixText,
@@ -63,6 +73,8 @@ abstract class InputDecorationBuilder {
errorBorder: errorBorder,
errorText: errorText,
errorStyle: errorTextStyle,
- enabledBorder: enabledBorder);
+ enabledBorder: enabledBorder,
+ focusedBorder: focusBorder
+ );
}
}
\ No newline at end of file
diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart
index b36cdd422..3caba72c5 100644
--- a/lib/features/login/presentation/login_controller.dart
+++ b/lib/features/login/presentation/login_controller.dart
@@ -6,6 +6,7 @@ import 'package:get/get.dart';
import 'package:model/model.dart';
import 'package:tmail_ui_user/features/login/domain/state/authentication_user_state.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart';
+import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -26,6 +27,7 @@ class LoginController extends GetxController {
);
var loginState = LoginState(Right(LoginInitAction())).obs;
+ final loginFormType = LoginFormType.baseUrlForm.obs;
String? _urlText;
String? _userNameText;
@@ -43,6 +45,20 @@ class LoginController extends GetxController {
Password? _parsePassword(String? password) => password != null ? Password(password.trim()) : null;
+ void handleNextInUrlInputFormPress() {
+ completeInputUrlAction();
+ }
+
+ void handleBackInCredentialForm() {
+ loginState.value = LoginState(Right(LoginInitAction()));
+ loginFormType.value = LoginFormType.baseUrlForm;
+ }
+
+ void completeInputUrlAction() {
+ loginState.value = LoginState(Right(InputUrlCompletion()));
+ loginFormType.value = LoginFormType.credentialForm;
+ }
+
void handleLoginPressed() {
final baseUri = kIsWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText);
final userName = _parseUserName(_userNameText);
@@ -82,7 +98,7 @@ class LoginController extends GetxController {
@override
void onClose() {
- urlInputController.dispose();
+ urlInputController.clear();
super.onClose();
}
}
\ No newline at end of file
diff --git a/lib/features/login/presentation/login_form_type.dart b/lib/features/login/presentation/login_form_type.dart
new file mode 100644
index 000000000..a5eb9d79b
--- /dev/null
+++ b/lib/features/login/presentation/login_form_type.dart
@@ -0,0 +1,3 @@
+enum LoginFormType {
+ none, baseUrlForm, credentialForm
+}
\ No newline at end of file
diff --git a/lib/features/login/presentation/login_view.dart b/lib/features/login/presentation/login_view.dart
index 0a4877560..80b3ce229 100644
--- a/lib/features/login/presentation/login_view.dart
+++ b/lib/features/login/presentation/login_view.dart
@@ -1,18 +1,20 @@
import 'package:core/core.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/login/domain/state/authentication_user_state.dart';
import 'package:tmail_ui_user/features/login/presentation/login_controller.dart';
+import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart';
import 'package:tmail_ui_user/features/login/presentation/widgets/login_input_decoration_builder.dart';
+import 'package:tmail_ui_user/features/login/presentation/widgets/login_text_input_builder.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class LoginView extends GetWidget {
final loginController = Get.find();
final imagePaths = Get.find();
- final responsiveUtils = Get.find();
+ final _responsiveUtils = Get.find();
final keyboardUtils = Get.find();
LoginView({Key? key}) : super(key: key);
@@ -20,97 +22,161 @@ class LoginView extends GetWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
- resizeToAvoidBottomInset: false,
+ resizeToAvoidBottomInset: true,
backgroundColor: AppColor.primaryLightColor,
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SafeArea(
- child: Center(
- child: SingleChildScrollView(
- reverse: true,
- child: Padding(
- padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
- child: Container(
- alignment: Alignment.center,
- padding: const EdgeInsets.only(top: 40),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- _buildSlogan(context),
- Obx(() => _buildLoginMessage(context, loginController.loginState.value)),
- if (!kIsWeb) _buildUrlInput(context),
- _buildUserNameInput(context),
- _buildPasswordInput(context),
- Obx(() => loginController.loginState.value.viewState.fold(
- (failure) => _buildLoginButton(context),
- (success) => success is LoginLoadingAction ? _buildLoadingCircularProgress() : _buildLoginButton(context))),
- ],
- ),
- ),
+ child: _supportScrollForm(context)
+ ? Stack(
+ children: [
+ Center(child: SingleChildScrollView(child: _buildCenterForm(context), scrollDirection: Axis.vertical)),
+ Obx(() {
+ return loginController.loginFormType.value == LoginFormType.credentialForm
+ ? _buildBackButton(context)
+ : const SizedBox.shrink();
+ })
+ ]
+ )
+ : Stack(
+ children: [
+ _buildCenterForm(context),
+ Obx(() {
+ return loginController.loginFormType.value == LoginFormType.credentialForm
+ ? _buildBackButton(context)
+ : const SizedBox.shrink();
+ })
+ ]
+ ),
+ ),
+ ));
+ }
+
+ Widget _buildCenterForm(BuildContext context) {
+ return Center(
+ child: ConstrainedBox(
+ constraints: _supportScrollForm(context)
+ ? const BoxConstraints(minWidth: 240, maxWidth: 400)
+ : const BoxConstraints(minWidth: double.infinity),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Padding(
+ padding: EdgeInsets.only(
+ top: _responsiveUtils.isHeightShortest(context) ? 64 : 0),
+ child: Text(
+ AppLocalizations.of(context).login,
+ style: const TextStyle(fontSize: 32, color: AppColor.colorNameEmail, fontWeight: FontWeight.w900)
)
),
- ),
+ Obx(() => _buildLoginMessage(context, loginController.loginState.value)),
+ Obx(() {
+ switch (controller.loginFormType.value) {
+ case LoginFormType.baseUrlForm:
+ return _buildUrlInput(context);
+ case LoginFormType.credentialForm:
+ return _buildInputCredentialForm(context);
+ default:
+ return const SizedBox.shrink();
+ }
+ }),
+ Obx(() {
+ switch (controller.loginFormType.value) {
+ case LoginFormType.baseUrlForm:
+ return _supportScrollForm(context)
+ ? _buildNextButton(context)
+ : _buildExpandedButton(context, _buildNextButton(context));
+ case LoginFormType.credentialForm:
+ return Obx(() => loginController.loginState.value.viewState.fold(
+ (failure) => _buildLoginButtonInContext(context),
+ (success) => success is LoginLoadingAction
+ ? _buildLoadingCircularProgress()
+ : _buildLoginButtonInContext(context)));
+ default:
+ return const SizedBox.shrink();
+ }
+ })
+ ]
),
- ),
+ )
);
}
- Widget _buildSlogan(BuildContext context) {
- return (SloganBuilder()
- ..setSloganText(AppLocalizations.of(context).login_text_slogan)
- ..setSloganTextAlign(TextAlign.center)
- ..setSloganTextStyle(const TextStyle(color: AppColor.primaryColor, fontSize: 16, fontWeight: FontWeight.w700))
- ..setLogo(imagePaths.icLogoTMail))
- .build();
+ Widget _buildBackButton(BuildContext context) {
+ return Positioned(
+ left: 12,
+ top: 12,
+ child: IconButton(
+ key: const Key('login_arrow_back_button'),
+ onPressed: () => controller.handleBackInCredentialForm(),
+ icon: SvgPicture.asset(
+ imagePaths.icBack,
+ alignment: Alignment.center,
+ color: AppColor.primaryColor
+ )
+ ),
+ );
}
Widget _buildLoginMessage(BuildContext context, LoginState loginState) {
return Padding(
- padding: const EdgeInsets.only(bottom: 24, top: 60),
+ padding: const EdgeInsets.only(top: 11, bottom: 36, left: 58, right: 58),
child: SizedBox(
- width: responsiveUtils.getWidthLoginTextField(context),
+ width: _responsiveUtils.getWidthLoginTextField(context),
child: CenterTextBuilder()
.key(const Key('login_message'))
.text(loginState.viewState.fold(
(failure) => failure is AuthenticationUserFailure
? AppLocalizations.of(context).unknown_error_login_message
- : AppLocalizations.of(context).login_text_login_to_continue,
- (success) => AppLocalizations.of(context).login_text_login_to_continue))
+ : AppLocalizations.of(context).loginInputUrlMessage,
+ (success) => AppLocalizations.of(context).loginInputUrlMessage))
.textStyle(TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w400,
color: loginState.viewState.fold(
(failure) => failure is AuthenticationUserFailure
? AppColor.textFieldErrorBorderColor
- : AppColor.appColor,
- (success) => AppColor.appColor)))
+ : AppColor.colorNameEmail,
+ (success) => AppColor.colorNameEmail)))
.build()
)
);
}
+ Widget _buildInputCredentialForm(BuildContext context) {
+ return Column(
+ children: [
+ _buildUserNameInput(context),
+ _buildPasswordInput(context)
+ ],
+ );
+ }
+
Widget _buildUrlInput(BuildContext context) {
return Padding(
- padding: const EdgeInsets.only(bottom: 16),
- child: SizedBox(
- width: responsiveUtils.getWidthLoginTextField(context),
+ padding: const EdgeInsets.only(right: 24, left: 24, bottom: 24),
+ child: Container(
child: (TextFieldBuilder()
..key(const Key('login_url_input'))
..onChange((value) => loginController.formatUrl(value))
..textInputAction(TextInputAction.next)
..addController(loginController.urlInputController)
..keyboardType(TextInputType.url)
+ ..onSubmitted((value) {
+ controller.handleNextInUrlInputFormPress();
+ })
..textDecoration((LoginInputDecorationBuilder()
- ..setLabelText(AppLocalizations.of(context).prefix_https)
- ..setPrefixText(AppLocalizations.of(context).prefix_https))
- .build()))
+ ..setLabelText(AppLocalizations.of(context).prefix_https)
+ ..setPrefixText(AppLocalizations.of(context).prefix_https))
+ .build()))
.build()));
}
Widget _buildUserNameInput(BuildContext context) {
return Padding(
- padding: const EdgeInsets.only(bottom: 16),
- child: SizedBox(
- width: responsiveUtils.getWidthLoginTextField(context),
+ padding: const EdgeInsets.only(bottom: 16, right: 24, left: 24),
+ child: Container(
child: (TextFieldBuilder()
..key(const Key('login_username_input'))
..onChange((value) => loginController.setUserNameText(value))
@@ -125,40 +191,78 @@ class LoginView extends GetWidget {
Widget _buildPasswordInput(BuildContext context) {
return Padding(
- padding: const EdgeInsets.only(bottom: 40),
- child: SizedBox(
- width: responsiveUtils.getWidthLoginTextField(context),
- child: (TextFieldBuilder()
+ padding: const EdgeInsets.only(bottom: 40, right: 24, left: 24),
+ child: Container(
+ child: (LoginTextInputBuilder(context, imagePaths)
+ ..setOnSubmitted((value) => loginController.handleLoginPressed())
+ ..passwordInput(true)
..key(const Key('login_password_input'))
..obscureText(true)
..onChange((value) => loginController.setPasswordText(value))
..textInputAction(TextInputAction.done)
- ..textDecoration((LoginInputDecorationBuilder()
- ..setLabelText(AppLocalizations.of(context).password)
- ..setHintText(AppLocalizations.of(context).password))
- .build()))
+ ..hintText(AppLocalizations.of(context).password))
.build()));
}
+ Widget _buildExpandedButton(BuildContext context, Widget child) {
+ return Expanded(
+ child: Align(
+ alignment: FractionalOffset.bottomCenter,
+ child: child
+ )
+ );
+ }
+
+ Widget _buildNextButton(BuildContext context) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24),
+ width: _responsiveUtils.getDeviceWidth(context),height: 48,
+ child: ElevatedButton(
+ key: const Key('nextToCredentialForm'),
+ style: ButtonStyle(
+ foregroundColor: MaterialStateProperty.resolveWith((Set states) => Colors.white),
+ backgroundColor: MaterialStateProperty.resolveWith((Set states) => AppColor.primaryColor),
+ shape: MaterialStateProperty.all(RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ side: const BorderSide(width: 0, color: AppColor.primaryColor)
+ ))
+ ),
+ child: Text(AppLocalizations.of(context).next,
+ style: const TextStyle(fontSize: 16, color: Colors.white)
+ ),
+ onPressed: () {
+ loginController.handleNextInUrlInputFormPress();
+ }
+ )
+ );
+ }
+
+ Widget _buildLoginButtonInContext(BuildContext context) {
+ return _supportScrollForm(context)
+ ? _buildLoginButton(context)
+ : _buildExpandedButton(context, _buildLoginButton(context));
+ }
+
Widget _buildLoginButton(BuildContext context) {
- return Padding(
- padding: const EdgeInsets.only(bottom: 16),
- child: SizedBox(
- key: const Key('login_confirm_button'),
- width: responsiveUtils.getWidthLoginButton(),
- height: 48,
- child: ElevatedButton(
- style: ButtonStyle(
- foregroundColor: MaterialStateProperty.resolveWith((Set states) => Colors.white),
- backgroundColor: MaterialStateProperty.resolveWith((Set states) => AppColor.buttonColor),
- shape: MaterialStateProperty.all(RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(80),
- side: const BorderSide(width: 0, color: AppColor.buttonColor)))),
- child: Text(AppLocalizations.of(context).login, style: const TextStyle(fontSize: 16, color: Colors.white)),
- onPressed: () {
- keyboardUtils.hideKeyboard(context);
- loginController.handleLoginPressed();
- }),
+ return Container(
+ margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24),
+ width: _responsiveUtils.getDeviceWidth(context),height: 48,
+ child: ElevatedButton(
+ key: const Key('loginSubmitForm'),
+ style: ButtonStyle(
+ foregroundColor: MaterialStateProperty.resolveWith((Set states) => Colors.white),
+ backgroundColor: MaterialStateProperty.resolveWith((Set states) => AppColor.primaryColor),
+ shape: MaterialStateProperty.all(RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ side: const BorderSide(width: 0, color: AppColor.primaryColor)
+ ))
+ ),
+ child: Text(AppLocalizations.of(context).login,
+ style: const TextStyle(fontSize: 16, color: Colors.white)
+ ),
+ onPressed: () {
+ loginController.handleLoginPressed();
+ }
)
);
}
@@ -170,4 +274,8 @@ class LoginView extends GetWidget {
height: 40,
child: CircularProgressIndicator(color: AppColor.primaryColor));
}
+
+ bool _supportScrollForm(BuildContext context) {
+ return !(responsiveUtils.isMobile(context) && responsiveUtils.isPortrait(context));
+ }
}
\ No newline at end of file
diff --git a/lib/features/login/presentation/state/login_state.dart b/lib/features/login/presentation/state/login_state.dart
index edbe51673..57474b060 100644
--- a/lib/features/login/presentation/state/login_state.dart
+++ b/lib/features/login/presentation/state/login_state.dart
@@ -7,6 +7,12 @@ class LoginState extends AppState {
LoginState(Either viewState) : super(viewState);
}
+@immutable
+class InputUrlCompletion extends ViewState {
+ @override
+ List