From 09dc37d5bde993c6153125e082e3b1d939222481 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 27 Jul 2021 12:55:23 +0700 Subject: [PATCH] Init login feature add presentation layer --- .../login/presentation/login_bindings.dart | 34 ++++ .../login/presentation/login_controller.dart | 69 +++++++ .../login/presentation/login_view.dart | 168 ++++++++++++++++++ .../login/presentation/state/login_state.dart | 28 +++ .../login_input_decoration_builder.dart | 64 +++++++ 5 files changed, 363 insertions(+) create mode 100644 lib/features/login/presentation/login_bindings.dart create mode 100644 lib/features/login/presentation/login_controller.dart create mode 100644 lib/features/login/presentation/login_view.dart create mode 100644 lib/features/login/presentation/state/login_state.dart create mode 100644 lib/features/login/presentation/widgets/login_input_decoration_builder.dart diff --git a/lib/features/login/presentation/login_bindings.dart b/lib/features/login/presentation/login_bindings.dart new file mode 100644 index 000000000..74dec5811 --- /dev/null +++ b/lib/features/login/presentation/login_bindings.dart @@ -0,0 +1,34 @@ +import 'package:core/core.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tmail_ui_user/features/login/data/datasource/atuthentitcation_datasource.dart'; +import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_datasource_impl.dart'; +import 'package:tmail_ui_user/features/login/data/network/login_api.dart'; +import 'package:tmail_ui_user/features/login/data/repository/authentication_repository_impl.dart'; +import 'package:tmail_ui_user/features/login/data/repository/credential_repository_impl.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; +import 'package:tmail_ui_user/features/login/presentation/login_controller.dart'; + +class LoginBindings extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AuthenticationDataSourceImpl(Get.find())); + Get.lazyPut(() => Get.find()); + + Get.lazyPut(() => CredentialRepositoryImpl(Get.find())); + Get.lazyPut(() => Get.find()); + + Get.lazyPut(() => AuthenticationRepositoryImpl(Get.find())); + Get.lazyPut(() => Get.find()); + + Get.lazyPut(() => AuthenticationInteractor(Get.find(), Get.find())); + + Get.lazyPut(() => LoginController( + Get.find(), + Get.find(), + Get.find()) + ); + } +} \ No newline at end of file diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart new file mode 100644 index 000000000..69046102d --- /dev/null +++ b/lib/features/login/presentation/login_controller.dart @@ -0,0 +1,69 @@ +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +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/state/login_state.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; + +class LoginController extends GetxController { + + final AuthenticationInteractor _authenticationInteractor; + final DynamicUrlInterceptors _dynamicUrlInterceptors; + final AuthorizationInterceptors _authorizationInterceptors; + + LoginController( + this._authenticationInteractor, + this._dynamicUrlInterceptors, + this._authorizationInterceptors, + ); + + var loginState = LoginState(Right(LoginInitAction())).obs; + + String? _urlText; + String? _userNameText; + String? _passwordText; + + void setUrlText(String url) => _urlText = url.formatURLValid(); + + void setUserNameText(String userName) => _userNameText = userName; + + void setPasswordText(String password) => _passwordText = password; + + Uri? _parseUri(String? url) => url != null ? Uri.parse(url) : null; + + UserName? _parseUserName(String? userName) => userName != null ? UserName(userName) : null; + + Password? _parsePassword(String? password) => password != null ? Password(password) : null; + + void handleLoginPressed() { + final baseUri = _parseUri(_urlText); + final userName = _parseUserName(_userNameText); + final password = _parsePassword(_passwordText); + if (baseUri != null && userName != null && password != null) { + _loginAction(baseUri, userName, password); + } else { + loginState.value = LoginState(Left(LoginMissPropertiesAction())); + } + } + + void _loginAction(Uri baseUrl, UserName userName, Password password) async { + loginState.value = LoginState(Right(LoginLoadingAction())); + await _authenticationInteractor.execute(baseUrl, userName, password) + .then((response) => response.fold( + (failure) => failure is AuthenticationUserFailure ? _loginFailureAction(failure) : null, + (success) => success is AuthenticationUserViewState ? _loginSuccessAction(success) : null)); + } + + void _loginSuccessAction(AuthenticationUserViewState success) { + loginState.value = LoginState(Right(success)); + _dynamicUrlInterceptors.changeBaseUrl(_urlText); + _authorizationInterceptors.changeAuthorization(_userNameText, _passwordText); + Get.offNamed(AppRoutes.MAILBOX); + } + + void _loginFailureAction(AuthenticationUserFailure failure) { + loginState.value = LoginState(Left(failure)); + } +} \ No newline at end of file diff --git a/lib/features/login/presentation/login_view.dart b/lib/features/login/presentation/login_view.dart new file mode 100644 index 000000000..1f0109dd2 --- /dev/null +++ b/lib/features/login/presentation/login_view.dart @@ -0,0 +1,168 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.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/state/login_state.dart'; +import 'package:tmail_ui_user/features/login/presentation/widgets/login_input_decoration_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 keyboardUtils = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + 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: EdgeInsets.only(top: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildSlogan(context), + Obx(() => _buildLoginMessage(context, loginController.loginState.value)), + _buildUrlInput(context), + _buildUserNameInput(context), + _buildPasswordInput(context), + Obx(() => loginController.loginState.value.viewState.fold( + (failure) => _buildLoginButton(context), + (success) => success is LoginLoadingAction ? _buildLoadingCircularProgress() : _buildLoginButton(context))), + ], + ), + ), + ) + ), + ), + ), + ), + ); + } + + Widget _buildSlogan(BuildContext context) { + return SloganBuilder() + .setSloganText(AppLocalizations.of(context).login_text_slogan) + .setSloganTextAlign(TextAlign.center) + .setSloganTextStyle(TextStyle(color: AppColor.primaryColor, fontSize: 16, fontWeight: FontWeight.w700)) + .setLogo(imagePaths.icTMailLogo) + .build(); + } + + Widget _buildLoginMessage(BuildContext context, LoginState loginState) { + return Padding( + padding: EdgeInsets.only(bottom: 24, top: 60), + child: Container( + width: responsiveUtils.getWidthLoginTextField(context), + child: CenterTextBuilder() + .key(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)) + .textStyle(TextStyle( + color: loginState.viewState.fold( + (failure) => failure is AuthenticationUserFailure + ? AppColor.textFieldErrorBorderColor + : AppColor.appColor, + (success) => AppColor.appColor))) + .build() + ) + ); + } + + Widget _buildUrlInput(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 16), + child: Container( + width: responsiveUtils.getWidthLoginTextField(context), + child: TextFieldBuilder() + .key(Key('login_url_input')) + .onChange((value) => loginController.setUrlText(value)) + .textInputAction(TextInputAction.next) + .textDecoration(LoginInputDecorationBuilder() + .setLabelText(AppLocalizations.of(context).prefix_https) + .setPrefixText(AppLocalizations.of(context).prefix_https) + .build()) + .build())); + } + + Widget _buildUserNameInput(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 16), + child: Container( + width: responsiveUtils.getWidthLoginTextField(context), + child: TextFieldBuilder() + .key(Key('login_username_input')) + .onChange((value) => loginController.setUserNameText(value)) + .textInputAction(TextInputAction.next) + .textDecoration(LoginInputDecorationBuilder() + .setLabelText(AppLocalizations.of(context).username) + .setHintText(AppLocalizations.of(context).username) + .build()) + .build())); + } + + Widget _buildPasswordInput(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 40), + child: Container( + width: responsiveUtils.getWidthLoginTextField(context), + child: TextFieldBuilder() + .key(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()) + .build())); + } + + Widget _buildLoginButton(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 16), + child: SizedBox( + key: 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: BorderSide(width: 0, color: AppColor.buttonColor)))), + child: Text(AppLocalizations.of(context).login, style: TextStyle(fontSize: 16, color: Colors.white)), + onPressed: () { + keyboardUtils.hideKeyboard(context); + loginController.handleLoginPressed(); + }), + ) + ); + } + + Widget _buildLoadingCircularProgress() { + return SizedBox( + key: Key('login_loading_icon'), + width: 40, + height: 40, + child: CircularProgressIndicator(color: AppColor.primaryColor)); + } +} \ 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 new file mode 100644 index 000000000..0dbbede92 --- /dev/null +++ b/lib/features/login/presentation/state/login_state.dart @@ -0,0 +1,28 @@ +import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; + +@immutable +class LoginState extends AppState { + LoginState(Either viewState) : super(viewState); +} + +@immutable +class LoginLoadingAction extends ViewState { + @override + List get props => []; +} + +@immutable +class LoginInitAction extends ViewState { + @override + List get props => []; +} + +@immutable +class LoginMissPropertiesAction extends Failure { + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/login/presentation/widgets/login_input_decoration_builder.dart b/lib/features/login/presentation/widgets/login_input_decoration_builder.dart new file mode 100644 index 000000000..25373623e --- /dev/null +++ b/lib/features/login/presentation/widgets/login_input_decoration_builder.dart @@ -0,0 +1,64 @@ +// LinShare is an open source filesharing software, part of the LinPKI software +// suite, developed by Linagora. +// +// Copyright (C) 2020 LINAGORA +// +// This program is free software: you can redistribute it and/or modify it under the +// terms of the GNU Affero General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version, +// provided you comply with the Additional Terms applicable for LinShare software by +// Linagora pursuant to Section 7 of the GNU Affero General Public License, +// subsections (b), (c), and (e), pursuant to which you must notably (i) retain the +// display in the interface of the “LinShare™” trademark/logo, the "Libre & Free" mention, +// the words “You are using the Free and Open Source version of LinShare™, powered by +// Linagora © 2009–2020. Contribute to Linshare R&D by subscribing to an Enterprise +// offer!”. You must also retain the latter notice in all asynchronous messages such as +// e-mails sent with the Program, (ii) retain all hypertext links between LinShare and +// http://www.linshare.org, between linagora.com and Linagora, and (iii) refrain from +// infringing Linagora intellectual property rights over its trademarks and commercial +// brands. Other Additional Terms apply, see +//