diff --git a/core/lib/utils/file_utils.dart b/core/lib/utils/file_utils.dart index 6f2cac4ea..aa9044205 100644 --- a/core/lib/utils/file_utils.dart +++ b/core/lib/utils/file_utils.dart @@ -105,7 +105,7 @@ class FileUtils { log('FileUtils::removeFolder():folderPath: $folderPath'); final dir = Directory(folderPath); if (await dir.exists()) { - await dir.delete(recursive: true); + await dir.delete(recursive: true); } } catch (e) { logError('FileUtils::removeFolder():EXCEPTION: $e'); diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 961cd6337..99baa3415 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -141,21 +141,7 @@ abstract class BaseController extends GetxController ); } return error; - } else if (error is BadCredentialsException) { - if (currentOverlayContext != null && currentContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).badCredentials - ); - } - return error; - } else if (error is ConnectionError) { - if (authorizationInterceptors.isAppRunning && currentOverlayContext != null && currentContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).connectionError - ); - } + } else if (error is BadCredentialsException || error is ConnectionError) { return error; } @@ -166,6 +152,22 @@ abstract class BaseController extends GetxController void handleExceptionAction({Failure? failure, Exception? exception}) { logError('BaseController::handleExceptionAction():failure: $failure | exception: $exception'); + if (exception is BadCredentialsException) { + if (currentOverlayContext != null && currentContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).badCredentials + ); + } + } else if (exception is ConnectionError) { + if (currentOverlayContext != null && currentContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).connectionError + ); + } + } + if (!authorizationInterceptors.isAppRunning) { return; } @@ -182,7 +184,7 @@ abstract class BaseController extends GetxController } else { await clearDataAndGoToLoginPage(); } - } else if (failure is GetFCMSubscriptionLocalFailure || + } else if (failure is GetFCMSubscriptionLocalFailure || failure is DestroySubscriptionFailure) { await clearDataAndGoToLoginPage(); } @@ -326,9 +328,9 @@ abstract class BaseController extends GetxController log('BaseController::clearDataAndGoToLoginPage:'); await clearAllData(); goToLogin(arguments: LoginArguments( - isAuthenticatedWithOidc - ? LoginFormType.ssoForm - : LoginFormType.credentialForm + PlatformInfo.isWeb + ? LoginFormType.none + : LoginFormType.dnsLookupForm )); } @@ -346,7 +348,9 @@ abstract class BaseController extends GetxController cachingManager.clearAll(), languageCacheManager.removeLanguage(), ]); - await cachingManager.clearAllFileInStorage(); + if (PlatformInfo.isMobile) { + await cachingManager.clearAllFileInStorage(); + } authorizationInterceptors.clear(); authorizationIsolateInterceptors.clear(); if (_isFcmEnabled) { @@ -361,7 +365,9 @@ abstract class BaseController extends GetxController cachingManager.clearAll(), languageCacheManager.removeLanguage(), ]); - await cachingManager.clearAllFileInStorage(); + if (PlatformInfo.isMobile) { + await cachingManager.clearAllFileInStorage(); + } authorizationIsolateInterceptors.clear(); authorizationInterceptors.clear(); if (_isFcmEnabled) { diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index 087829c89..cee16def9 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -1,7 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; @@ -10,11 +10,9 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; -import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart'; import 'package:tmail_ui_user/features/home/domain/usecases/get_session_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_authenticated_account_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_state.dart'; @@ -24,9 +22,9 @@ import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_interactors_bindings.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; -import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/utils/message_toast_utils.dart'; abstract class ReloadableController extends BaseController { final GetSessionInteractor _getSessionInteractor = Get.find(); @@ -37,16 +35,19 @@ abstract class ReloadableController extends BaseController { void handleFailureViewState(Failure failure) { super.handleFailureViewState(failure); if (failure is GetCredentialFailure) { - goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); + goToLogin(arguments: LoginArguments(LoginFormType.dnsLookupForm)); } else if (failure is GetSessionFailure) { - _handleGetSessionFailure( - sessionCacheException: failure.exception, - sessionRemoteException: failure.remoteException - ); + _handleGetSessionFailure(failure.exception); } else if (failure is GetStoredTokenOidcFailure) { goToLogin(arguments: LoginArguments(LoginFormType.none)); } else if (failure is GetAuthenticatedAccountFailure) { - goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); + goToLogin( + arguments: LoginArguments( + PlatformInfo.isMobile + ? LoginFormType.dnsLookupForm + : LoginFormType.none + ) + ); } } @@ -95,56 +96,16 @@ abstract class ReloadableController extends BaseController { consumeState(_getSessionInteractor.execute()); } - void _handleGetSessionFailure({ - dynamic sessionRemoteException, - dynamic sessionCacheException - }) { - showToastErrorMessageSessionFailure( - sessionRemoteException: sessionRemoteException, - sessionCacheException: sessionCacheException, - ); + void _handleGetSessionFailure(dynamic exception) { + if (currentContext != null || currentOverlayContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + MessageToastUtils.getMessageByException(currentContext!, exception) ?? AppLocalizations.of(currentContext!).unknownError + ); + } clearDataAndGoToLoginPage(); } - void showToastErrorMessageSessionFailure({ - dynamic sessionRemoteException, - dynamic sessionCacheException - }) { - if (currentContext == null || currentOverlayContext == null) { - return; - } - appToast.showToastErrorMessage( - currentOverlayContext!, - getErrorMessageFromSessionFailure( - context: currentContext!, - sessionRemoteException: sessionRemoteException, - sessionCacheException: sessionCacheException, - ) - ); - } - - String getErrorMessageFromSessionFailure({ - required BuildContext context, - dynamic sessionRemoteException, - dynamic sessionCacheException, - }) { - if (sessionRemoteException is ConnectionTimeout || - sessionRemoteException is BadGateway || - sessionRemoteException is SocketError) { - return AppLocalizations.of(context).wrongUrlMessage; - } else if (sessionRemoteException is BadCredentialsException) { - return AppLocalizations.of(context).badCredentials; - } else if (sessionRemoteException is ConnectionError) { - return AppLocalizations.of(context).connectionError; - } else if (sessionRemoteException is UnknownError && sessionRemoteException.message != null) { - return '[${sessionRemoteException.code ?? ''}] ${sessionRemoteException.message}'; - } else if (sessionCacheException is NotFoundSessionException) { - return AppLocalizations.of(context).notFoundSession; - } else { - return AppLocalizations.of(context).unknownError; - } - } - void _handleGetSessionSuccess(GetSessionSuccess success) { final session = success.session; final personalAccount = session.personalAccount; diff --git a/lib/features/base/widget/recent_item_tile_widget.dart b/lib/features/base/widget/recent_item_tile_widget.dart index 1000e252b..b3bc96173 100644 --- a/lib/features/base/widget/recent_item_tile_widget.dart +++ b/lib/features/base/widget/recent_item_tile_widget.dart @@ -2,6 +2,7 @@ import 'package:core/presentation/resources/image_paths.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/model/recent_login_url.dart'; import 'package:tmail_ui_user/features/login/domain/model/recent_login_username.dart'; @@ -9,13 +10,15 @@ class RecentItemTileWidget extends StatelessWidget { final T item; final EdgeInsets? contentPadding; - final ImagePaths imagePath; + final _imagePath = Get.find(); - const RecentItemTileWidget(this.item, { - required this.imagePath, + RecentItemTileWidget( + this.item, + { Key? key, this.contentPadding, - }) : super(key: key); + } + ) : super(key: key); @override Widget build(BuildContext context) { @@ -23,14 +26,17 @@ class RecentItemTileWidget extends StatelessWidget { padding: contentPadding ?? const EdgeInsets.all(12), child: Row( children: [ - SvgPicture.asset(imagePath.icClockSB), + SvgPicture.asset(_imagePath.icClockSB), const SizedBox(width: 8), Expanded( - child: Text(_getTitle(item), - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: Colors.black)), + child: Text( + _getTitle(item), + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black + ) + ), ), ], ), diff --git a/lib/features/caching/caching_manager.dart b/lib/features/caching/caching_manager.dart index 3fbbf3e9f..bfc4447c1 100644 --- a/lib/features/caching/caching_manager.dart +++ b/lib/features/caching/caching_manager.dart @@ -129,12 +129,10 @@ class CachingManager { } Future clearAllFileInStorage() async { - if (PlatformInfo.isMobile) { - await Future.wait([ - _fileUtils.removeFolder(CachingConstants.newEmailsContentFolderName), - _fileUtils.removeFolder(CachingConstants.openedEmailContentFolderName), - ]); - } + await Future.wait([ + _fileUtils.removeFolder(CachingConstants.newEmailsContentFolderName), + _fileUtils.removeFolder(CachingConstants.openedEmailContentFolderName), + ]); } Future _clearSendingEmailCache() async { diff --git a/lib/features/home/domain/state/get_session_state.dart b/lib/features/home/domain/state/get_session_state.dart index 569344b36..bd9247665 100644 --- a/lib/features/home/domain/state/get_session_state.dart +++ b/lib/features/home/domain/state/get_session_state.dart @@ -14,7 +14,6 @@ class GetSessionSuccess extends UIState { } class GetSessionFailure extends FeatureFailure { - final dynamic remoteException; - GetSessionFailure({dynamic exception, this.remoteException}) : super(exception: exception); + GetSessionFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/home/domain/usecases/get_session_interactor.dart b/lib/features/home/domain/usecases/get_session_interactor.dart index c450da9d2..e81f774bf 100644 --- a/lib/features/home/domain/usecases/get_session_interactor.dart +++ b/lib/features/home/domain/usecases/get_session_interactor.dart @@ -20,22 +20,19 @@ class GetSessionInteractor { if (PlatformInfo.isMobile) { yield* _getStoredSessionFromCache(remoteException: e); } else { - yield Left(GetSessionFailure(exception: e)); + yield Left(GetSessionFailure(e)); } } } Stream> _getStoredSessionFromCache({dynamic remoteException}) async* { try { - log('GetSessionInteractor::_getStoredSessionFromCache:'); + log('GetSessionInteractor::_getStoredSessionFromCache:remoteException: $remoteException'); yield Right(GetSessionLoading()); final session = await sessionRepository.getStoredSession(); yield Right(GetSessionSuccess(session)); } catch (e) { - yield Left(GetSessionFailure( - exception: e, - remoteException: remoteException - )); + yield Left(GetSessionFailure(remoteException ?? e)); } } } \ No newline at end of file diff --git a/lib/features/login/data/network/dns_service.dart b/lib/features/login/data/network/dns_service.dart index 7b33ebbd5..c7aab4b4d 100644 --- a/lib/features/login/data/network/dns_service.dart +++ b/lib/features/login/data/network/dns_service.dart @@ -46,6 +46,9 @@ class DNSService { } final url = _removeDotAtEndOfString(listFieldData.last); log('DNSService::_parsingUrlFromDataResourceRecord:url: $url'); + if (url.trim().isEmpty) { + throw NotFoundUrlException(); + } return url; } diff --git a/lib/features/login/domain/usecases/authentication_user_interactor.dart b/lib/features/login/domain/usecases/authentication_user_interactor.dart index 5e698e41b..5b56a896e 100644 --- a/lib/features/login/domain/usecases/authentication_user_interactor.dart +++ b/lib/features/login/domain/usecases/authentication_user_interactor.dart @@ -1,13 +1,11 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/account/password.dart'; import 'package:model/account/personal_account.dart'; import 'package:tmail_ui_user/features/login/data/model/authentication_info_cache.dart'; -import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.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'; @@ -24,33 +22,25 @@ class AuthenticationInteractor { this._accountRepository ); - Stream> execute({Uri? baseUrl, UserName? userName, Password? password}) async* { + Stream> execute({ + required Uri baseUrl, + required UserName userName, + required Password password + }) async* { try { yield Right(AuthenticationUserLoading()); - - if (baseUrl != null && userName != null && password != null) { - final user = await authenticationRepository.authenticationUser(baseUrl, userName, password); - await Future.wait([ - credentialRepository.saveBaseUrl(baseUrl), - credentialRepository.storeAuthenticationInfo(AuthenticationInfoCache(userName.value, password.value)), - _accountRepository.setCurrentAccount(PersonalAccount( - userName.value, - AuthenticationType.basic, - isSelected: true - )) - ]); - yield Right(AuthenticationUserSuccess(user)); - } else if (baseUrl == null) { - yield Left(AuthenticationUserFailure(CanNotFoundBaseUrl())); - } else if (userName == null) { - yield Left(AuthenticationUserFailure(CanNotFoundUserName())); - } else if (password == null) { - yield Left(AuthenticationUserFailure(CanNotFoundPassword())); - } else { - yield Left(AuthenticationUserFailure(null)); - } + final user = await authenticationRepository.authenticationUser(baseUrl, userName, password); + await Future.wait([ + credentialRepository.saveBaseUrl(baseUrl), + credentialRepository.storeAuthenticationInfo(AuthenticationInfoCache(userName.value, password.value)), + _accountRepository.setCurrentAccount(PersonalAccount( + userName.value, + AuthenticationType.basic, + isSelected: true + )) + ]); + yield Right(AuthenticationUserSuccess(user)); } catch (e) { - logError('AuthenticationInteractor::execute(): $e'); yield Left(AuthenticationUserFailure(e)); } } diff --git a/lib/features/login/presentation/base_login_view.dart b/lib/features/login/presentation/base_login_view.dart index 3b09a425f..8b4631b8f 100644 --- a/lib/features/login/presentation/base_login_view.dart +++ b/lib/features/login/presentation/base_login_view.dart @@ -1,24 +1,11 @@ import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/widget/recent_item_tile_widget.dart'; -import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart'; -import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; -import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/login/domain/model/recent_login_username.dart'; -import 'package:tmail_ui_user/features/login/domain/state/authenticate_oidc_on_browser_state.dart'; -import 'package:tmail_ui_user/features/login/domain/state/authentication_user_state.dart'; -import 'package:tmail_ui_user/features/login/domain/state/check_oidc_is_available_state.dart'; -import 'package:tmail_ui_user/features/login/domain/state/get_oidc_configuration_state.dart'; -import 'package:tmail_ui_user/features/login/domain/state/get_oidc_is_available_state.dart'; -import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_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/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'; @@ -26,85 +13,20 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; abstract class BaseLoginView extends GetWidget { const BaseLoginView({Key? key}) : super(key: key); - Widget buildLoginMessage(BuildContext context, Either viewState) { - return Padding( - padding: const EdgeInsets.only(top: 11, bottom: 36, left: 58, right: 58), - child: SizedBox( - width: controller.responsiveUtils.getWidthLoginTextField(context), - child: Text( - viewState.fold( - (failure) { - if (failure is CheckOIDCIsAvailableFailure) { - return _getMessageFailure(context, failure.exception); - } else if (failure is AuthenticationUserFailure) { - return _getMessageFailure(context, failure.exception); - } else if (failure is GetOIDCIsAvailableFailure) { - return _getMessageFailure(context, failure.exception); - } else if (failure is GetTokenOIDCFailure) { - return _getMessageFailure(context, failure.exception); - } else if (failure is AuthenticateOidcOnBrowserFailure) { - return _getMessageFailure(context, failure.exception); - } else if (failure is GetOIDCConfigurationFailure) { - return AppLocalizations.of(context).canNotVerifySSOConfiguration; - } else if (failure is GetSessionFailure) { - return controller.getErrorMessageFromSessionFailure( - context: context, - sessionRemoteException: failure.remoteException, - sessionCacheException: failure.exception - ); - } else { - return AppLocalizations.of(context).unknownError; - } - }, - (success) { - if (controller.loginFormType.value == LoginFormType.credentialForm) { - return AppLocalizations.of(context).loginInputCredentialMessage; - } else if (controller.loginFormType.value == LoginFormType.ssoForm) { - return AppLocalizations.of(context).loginInputSSOMessage; - } - return AppLocalizations.of(context).loginInputUrlMessage; - }), - key: const Key('login_message'), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w400, - color: viewState.fold( - (failure) => AppColor.textFieldErrorBorderColor, - (success) => AppColor.colorNameEmail)), - )) - ); - } - - String _getMessageFailure(BuildContext context, dynamic exception) { - if (exception is CanNotFoundBaseUrl) { - return AppLocalizations.of(context).requiredUrl; - } else if (exception is CanNotFoundUserName) { - return AppLocalizations.of(context).requiredEmail; - } else if (exception is CanNotFoundPassword) { - return AppLocalizations.of(context).requiredPassword; - } else if (exception is CanNotFoundOIDCLinks) { - return AppLocalizations.of(context).ssoNotAvailable; - } else if (exception is CanNotFoundToken) { - return AppLocalizations.of(context).canNotGetToken; - } else { - return ''; - } - } - Widget buildLoginButton(BuildContext context) { return Container( - margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24), - width: controller.responsiveUtils.getDeviceWidth(context),height: 48, + margin: const EdgeInsetsDirectional.only(bottom: 16, start: 24, end: 24), + width: controller.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( + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: AppColor.primaryColor, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: const BorderSide(width: 0, color: AppColor.primaryColor) - )) + ) ), onPressed: controller.handleLoginPressed, child: Text( @@ -135,7 +57,8 @@ abstract class BaseLoginView extends GetWidget { return TypeAheadFormFieldBuilder( key: const Key('login_username_input'), controller: controller.usernameInputController, - onTextChange: controller.setUserNameText, + onTextChange: controller.onUsernameChange, + focusNode: controller.userNameFocusNode, textInputAction: TextInputAction.next, autocorrect: false, autofillHints: const [AutofillHints.email], @@ -146,11 +69,8 @@ abstract class BaseLoginView extends GetWidget { .build(), debounceDuration: const Duration(milliseconds: 300), suggestionsCallback: controller.getAllRecentLoginUsernameAction, - itemBuilder: (context, loginUsername) => RecentItemTileWidget(loginUsername, imagePath: controller.imagePaths), - onSuggestionSelected: (recentUsername) { - controller.setUsername(recentUsername.username); - controller.passFocusNode.requestFocus(); - }, + itemBuilder: (_, loginUsername) => RecentItemTileWidget(loginUsername), + onSuggestionSelected: controller.selectUsernameFromSuggestion, suggestionsBoxDecoration: const SuggestionsBoxDecoration(borderRadius: BorderRadius.all(Radius.circular(14))), noItemsFoundBuilder: (context) => const SizedBox(), hideOnEmpty: true, @@ -167,8 +87,8 @@ abstract class BaseLoginView extends GetWidget { textInputAction: TextInputAction.done, hintText: AppLocalizations.of(context).password, focusNode: controller.passFocusNode, - onTextChange: controller.setPasswordText, - onSubmitted: (value) => controller.handleLoginPressed(), + onTextChange: controller.onPasswordChange, + onSubmitted: (_) => controller.handleLoginPressed(), ); } diff --git a/lib/features/login/presentation/login_bindings.dart b/lib/features/login/presentation/login_bindings.dart index 1dae7cd02..980cc98e7 100644 --- a/lib/features/login/presentation/login_bindings.dart +++ b/lib/features/login/presentation/login_bindings.dart @@ -1,21 +1,21 @@ +import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/caching/clients/recent_login_url_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/recent_login_username_cache_client.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/login_url_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/login_username_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/login_url_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/login_username_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/repository/login_url_repository_impl.dart'; -import 'package:tmail_ui_user/features/login/data/repository/login_username_repository_impl.dart'; +import 'package:tmail_ui_user/features/login/data/datasource/login_datasource.dart'; +import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_login_datasource_impl.dart'; +import 'package:tmail_ui_user/features/login/data/datasource_impl/login_datasource_impl.dart'; +import 'package:tmail_ui_user/features/login/data/network/dns_service.dart'; +import 'package:tmail_ui_user/features/login/data/repository/login_repository_impl.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/login_url_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/login_username_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/login_repository.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authenticate_oidc_on_browser_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/dns_lookup_to_get_jmap_url_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_username_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authentication_info_interactor.dart'; @@ -27,6 +27,7 @@ import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_m import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_controller.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; class LoginBindings extends BaseBindings { @@ -45,24 +46,26 @@ class LoginBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } @override void bindingsDataSource() { - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - + Get.lazyPut(() => Get.find()); } @override void bindingsDataSourceImpl() { - Get.lazyPut(() => LoginUrlDataSourceImpl( + Get.lazyPut(() => HiveLoginDataSourceImpl( Get.find(), - Get.find())); - Get.lazyPut(() => LoginUsernameDataSourceImpl( Get.find(), - Get.find())); + Get.find() + )); + Get.lazyPut(() => LoginDataSourceImpl( + Get.find(), + Get.find(), + )); } @override @@ -90,28 +93,25 @@ class LoginBindings extends BaseBindings { Get.lazyPut(() => GetStoredOidcConfigurationInteractor( Get.find(), )); - Get.lazyPut(() => SaveLoginUrlOnMobileInteractor( - Get.find(), - )); - Get.lazyPut(() => GetAllRecentLoginUrlOnMobileInteractor(Get.find())); - Get.lazyPut(() => SaveLoginUsernameOnMobileInteractor( - Get.find(), - )); - Get.lazyPut(() => GetAllRecentLoginUsernameOnMobileInteractor( - Get.find() - )); + Get.lazyPut(() => SaveLoginUrlOnMobileInteractor(Get.find(),)); + Get.lazyPut(() => GetAllRecentLoginUrlOnMobileInteractor(Get.find())); + Get.lazyPut(() => SaveLoginUsernameOnMobileInteractor(Get.find(),)); + Get.lazyPut(() => GetAllRecentLoginUsernameOnMobileInteractor(Get.find())); + Get.lazyPut(() => DNSLookupToGetJmapUrlInteractor(Get.find())); } @override void bindingsRepository() { - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - + Get.lazyPut(() => Get.find()); } @override void bindingsRepositoryImpl() { - Get.lazyPut(() => LoginUrlRepositoryImpl(Get.find())); - Get.lazyPut(() => LoginUsernameRepositoryImpl(Get.find())); + Get.lazyPut(() => LoginRepositoryImpl( + { + DataSourceType.hiveCache: Get.find(), + DataSourceType.network: 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 index 8d3b85e4c..b555f0777 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -24,6 +24,7 @@ import 'package:tmail_ui_user/features/login/domain/model/recent_login_username. import 'package:tmail_ui_user/features/login/domain/state/authenticate_oidc_on_browser_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/authentication_user_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/check_oidc_is_available_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/dns_lookup_to_get_jmap_url_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_all_recent_login_url_latest_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_all_recent_login_username_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_authenticated_account_state.dart'; @@ -35,6 +36,7 @@ import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_state.d import 'package:tmail_ui_user/features/login/domain/usecases/authenticate_oidc_on_browser_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/dns_lookup_to_get_jmap_url_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_username_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authentication_info_interactor.dart'; @@ -67,12 +69,22 @@ class LoginController extends ReloadableController { final GetAllRecentLoginUrlOnMobileInteractor _getAllRecentLoginUrlOnMobileInteractor; final SaveLoginUsernameOnMobileInteractor _saveLoginUsernameOnMobileInteractor; final GetAllRecentLoginUsernameOnMobileInteractor _getAllRecentLoginUsernameOnMobileInteractor; + final DNSLookupToGetJmapUrlInteractor _dnsLookupToGetJmapUrlInteractor; final TextEditingController urlInputController = TextEditingController(); final TextEditingController usernameInputController = TextEditingController(); final TextEditingController passwordInputController = TextEditingController(); + final FocusNode baseUrlFocusNode = FocusNode(); + final FocusNode userNameFocusNode = FocusNode(); final FocusNode passFocusNode = FocusNode(); + final loginFormType = LoginFormType.none.obs; + + OIDCResponse? _oidcResponse; + UserName? _username; + Password? _password; + Uri? _baseUri; + LoginController( this._authenticationInteractor, this._checkOIDCIsAvailableInteractor, @@ -86,42 +98,26 @@ class LoginController extends ReloadableController { this._getAllRecentLoginUrlOnMobileInteractor, this._saveLoginUsernameOnMobileInteractor, this._getAllRecentLoginUsernameOnMobileInteractor, + this._dnsLookupToGetJmapUrlInteractor, ); - final loginFormType = LoginFormType.baseUrlForm.obs; - - String? _urlText; - String? _userNameText; - String? _passwordText; - OIDCResponse? _oidcResponse; - - void setUrlText(String url) => _urlText = url.trim().formatURLValid(); - - void setUserNameText(String userName) => _userNameText = userName; - - void setPasswordText(String password) => _passwordText = password; - - Uri? _parseUri(String? url) => url != null && url.trim().isNotEmpty - ? Uri.parse(url.trim()) - : null; - - UserName? _parseUserName(String? userName) => userName != null && userName.trim().isNotEmpty - ? UserName(userName.trim()) - : null; - - Password? _parsePassword(String? password) => password != null && password.trim().isNotEmpty - ? Password(password.trim()) - : null; - @override void onReady() { super.onReady(); - if (PlatformInfo.isWeb) { - final arguments = Get.arguments; - if (arguments is LoginArguments) { - loginFormType.value = arguments.loginFormType; - _checkOIDCIsAvailable(); + final arguments = Get.arguments; + if (arguments is LoginArguments) { + if (arguments.loginFormType == LoginFormType.passwordForm) { + loginFormType.value = LoginFormType.dnsLookupForm; } else { + loginFormType.value = arguments.loginFormType; + } + if (PlatformInfo.isWeb) { + _checkOIDCIsAvailable(); + } else if (arguments.baseUrl?.isNotEmpty == true) { + onBaseUrlChange(arguments.baseUrl!); + } + } else { + if (PlatformInfo.isWeb) { _getAuthenticationInfo(); } } @@ -134,12 +130,21 @@ class LoginController extends ReloadableController { } else if (failure is CheckOIDCIsAvailableFailure || failure is GetStoredOidcConfigurationFailure || failure is GetOIDCIsAvailableFailure || + failure is GetOIDCConfigurationFailure || failure is GetTokenOIDCFailure) { - _showFormLoginWithCredentialAction(); + if (PlatformInfo.isMobile && loginFormType.value == LoginFormType.dnsLookupForm) { + _showPasswordForm(); + } else { + _showCredentialForm(); + } } else if (failure is GetAuthenticatedAccountFailure) { _checkOIDCIsAvailable(); } else if (failure is GetSessionFailure) { clearAllData(); + } else if (failure is DNSLookupToGetJmapUrlFailure) { + _username = null; + _clearTextInputField(); + _showBaseUrlForm(); } else { super.handleFailureViewState(failure); } @@ -162,6 +167,8 @@ class LoginController extends ReloadableController { _getTokenOIDCSuccess(success); } else if (success is AuthenticationUserSuccess) { _loginSuccessAction(success); + } else if (success is DNSLookupToGetJmapUrlSuccess) { + _handleDNSLookupToGetJmapUrlSuccess(success); } else { super.handleSuccessViewState(success); } @@ -169,14 +176,21 @@ class LoginController extends ReloadableController { @override void handleExceptionAction({Failure? failure, Exception? exception}) { - super.handleExceptionAction(failure: failure, exception: exception); + logError('LoginController::handleExceptionAction:exception: $exception | failure: ${failure.runtimeType}'); if (failure is CheckOIDCIsAvailableFailure || failure is GetStoredOidcConfigurationFailure || + failure is GetOIDCConfigurationFailure || failure is GetOIDCIsAvailableFailure || failure is GetTokenOIDCFailure) { - _showFormLoginWithCredentialAction(); + if (PlatformInfo.isMobile && loginFormType.value == LoginFormType.dnsLookupForm) { + _showPasswordForm(); + } else { + _showCredentialForm(); + } + } else if (failure is GetSessionFailure) { + clearAllData(); } else { - clearState(); + super.handleExceptionAction(failure: failure, exception: exception); } } @@ -197,19 +211,20 @@ class LoginController extends ReloadableController { } void handleNextInUrlInputFormPress() { - _saveRecentLoginUrl(); + if (PlatformInfo.isMobile && _currentBaseUrl != null) { + _storeBaseUrlToCache(_currentBaseUrl!); + } _checkOIDCIsAvailable(); } void _checkOIDCIsAvailable() { - final baseUrl = _getBaseUrl(); - if (baseUrl == null) { + if (_currentBaseUrl == null) { dispatchState(Left(CheckOIDCIsAvailableFailure(CanNotFoundBaseUrl()))); } else { consumeState(_checkOIDCIsAvailableInteractor.execute( OIDCRequest( - baseUrl: baseUrl.toString(), - resourceUrl: baseUrl.origin + baseUrl: _currentBaseUrl!.toString(), + resourceUrl: _currentBaseUrl!.origin ) )); } @@ -217,46 +232,60 @@ class LoginController extends ReloadableController { void _redirectToSSOLoginScreen(CheckOIDCIsAvailableSuccess success) { _oidcResponse = success.oidcResponse; - handleSSOPressed(); + consumeState(_getOIDCIsAvailableInteractor.execute( + OIDCRequest( + baseUrl: _currentBaseUrl!.toString(), + resourceUrl: _currentBaseUrl!.origin + ) + )); } - void handleBackInCredentialForm() { + void handleBackButtonAction() { clearState(); - loginFormType.value = LoginFormType.baseUrlForm; - } - - void _showFormLoginWithCredentialAction() { - clearState(); - loginFormType.value = LoginFormType.credentialForm; - } - - void handleLoginPressed() { - TextInput.finishAutofillContext(); - - _saveRecentLoginUsername(); - log('LoginController::handleLoginPressed(): ${loginFormType.value}'); - if (loginFormType.value == LoginFormType.ssoForm) { - _getOIDCConfiguration(); - } else { - final baseUrl = _getBaseUrl(); - final userName = _parseUserName(_userNameText); - final password = _parsePassword(_passwordText); - - _loginAction(baseUrl: baseUrl, userName: userName, password: password); + if (loginFormType.value == LoginFormType.credentialForm) { + _password = null; + _username = null; + usernameInputController.clear(); + passwordInputController.clear(); + loginFormType.value = LoginFormType.baseUrlForm; + } else if (loginFormType.value == LoginFormType.passwordForm) { + _password = null; + _baseUri = null; + urlInputController.clear(); + passwordInputController.clear(); + loginFormType.value = LoginFormType.dnsLookupForm; } } - void handleSSOPressed() { - final baseUrl = _getBaseUrl(); - if (baseUrl != null) { - consumeState(_getOIDCIsAvailableInteractor.execute( - OIDCRequest( - baseUrl: baseUrl.toString(), - resourceUrl: baseUrl.origin - ) - )); + void _showCredentialForm() { + clearState(); + loginFormType.value = LoginFormType.credentialForm; + userNameFocusNode.requestFocus(); + } + + void handleLoginPressed() { + log('LoginController::handleLoginPressed:_currentBaseUrl: $_currentBaseUrl | _username: $_username | _password: $_password'); + if (_currentBaseUrl == null) { + consumeState(Stream.value(Left(AuthenticationUserFailure(CanNotFoundBaseUrl())))); + } else if (_username == null) { + consumeState(Stream.value(Left(AuthenticationUserFailure(CanNotFoundUserName())))); + } else if (_password == null) { + consumeState(Stream.value(Left(AuthenticationUserFailure(CanNotFoundPassword())))); } else { - dispatchState(Left(GetOIDCIsAvailableFailure(CanNotFoundBaseUrl()))); + if (PlatformInfo.isMobile && loginFormType.value == LoginFormType.credentialForm) { + TextInput.finishAutofillContext(); + if (_username!.value.isEmail) { + _storeUsernameToCache(_username!.value); + } + } + + consumeState( + _authenticationInteractor.execute( + baseUrl: _currentBaseUrl!, + userName: _username!, + password: _password! + ) + ); } } @@ -264,12 +293,11 @@ class LoginController extends ReloadableController { if (_oidcResponse != null) { consumeState(_getOIDCConfigurationInteractor.execute(_oidcResponse!)); } else { - dispatchState(Left(GetOIDCIsAvailableFailure(CanNotFoundOIDCLinks()))); + dispatchState(Left(GetOIDCConfigurationFailure(CanNotFoundOIDCLinks()))); } } void _getOIDCConfigurationSuccess(GetOIDCConfigurationSuccess success) { - log('LoginController::_getOIDCConfigurationSuccess():success: $success'); if (PlatformInfo.isWeb) { _authenticateOidcOnBrowserAction(success.oidcConfiguration); } else { @@ -277,10 +305,9 @@ class LoginController extends ReloadableController { } } - void _getTokenOIDCAction(OIDCConfiguration config) async { - final baseUri = _getBaseUrl(); - if (baseUri != null) { - consumeState(_getTokenOIDCInteractor.execute(baseUri, config)); + void _getTokenOIDCAction(OIDCConfiguration config) { + if (_currentBaseUrl != null) { + consumeState(_getTokenOIDCInteractor.execute(_currentBaseUrl!, config)); } else { dispatchState(Left(GetTokenOIDCFailure(CanNotFoundBaseUrl()))); } @@ -289,9 +316,8 @@ class LoginController extends ReloadableController { void _authenticateOidcOnBrowserAction(OIDCConfiguration config) async { _removeAuthDestinationUrlInSessionStorage(); - final baseUri = _parseUri(AppConfig.baseUrl); - if (baseUri != null) { - consumeState(_authenticateOidcOnBrowserInteractor.execute(baseUri, config)); + if (_currentBaseUrl != null) { + consumeState(_authenticateOidcOnBrowserInteractor.execute(_currentBaseUrl!, config)); } else { dispatchState(Left(AuthenticateOidcOnBrowserFailure(CanNotFoundBaseUrl()))); } @@ -305,9 +331,8 @@ class LoginController extends ReloadableController { } void _getTokenOIDCSuccess(GetTokenOIDCSuccess success) { - log('LoginController::_getTokenOIDCSuccess(): ${success.tokenOIDC.toString()}'); - dynamicUrlInterceptors.setJmapUrl(_getBaseUrl().toString()); - dynamicUrlInterceptors.changeBaseUrl(_getBaseUrl().toString()); + dynamicUrlInterceptors.setJmapUrl(_currentBaseUrl?.toString()); + dynamicUrlInterceptors.changeBaseUrl(_currentBaseUrl?.toString()); authorizationInterceptors.setTokenAndAuthorityOidc( newToken: success.tokenOIDC.toToken(), newConfig: success.configuration); @@ -317,36 +342,24 @@ class LoginController extends ReloadableController { getSessionAction(); } - void _loginAction({Uri? baseUrl, UserName? userName, Password? password}) { - consumeState(_authenticationInteractor.execute( - baseUrl: baseUrl, - userName: userName, - password: password - )); - } - void _loginSuccessAction(AuthenticationUserSuccess success) { - dynamicUrlInterceptors.setJmapUrl(_getBaseUrl().toString()); - dynamicUrlInterceptors.changeBaseUrl(_getBaseUrl().toString()); - authorizationInterceptors.setBasicAuthorization(_userNameText, _passwordText); - authorizationIsolateInterceptors.setBasicAuthorization(_userNameText, _passwordText); + dynamicUrlInterceptors.setJmapUrl(_currentBaseUrl?.toString()); + dynamicUrlInterceptors.changeBaseUrl(_currentBaseUrl?.toString()); + authorizationInterceptors.setBasicAuthorization(_username!, _password!); + authorizationIsolateInterceptors.setBasicAuthorization(_username!, _password!); getSessionAction(); } - void formatUrl(String url) { - log('LoginController::formatUrl(): $url'); - if (url.isValid()) { - urlInputController.text = url.removePrefix(); - } - setUrlText(urlInputController.text); + void selectBaseUrlFromSuggestion(String url) { + final validUrl = url.isValid() ? url.removePrefix() : url; + log('LoginController::selectBaseUrlFromSuggestion:validUrl: $validUrl'); + urlInputController.text = validUrl; + onBaseUrlChange(validUrl); } - void _saveRecentLoginUrl() { - if (_urlText?.isNotEmpty == true && PlatformInfo.isMobile) { - final recentLoginUrl = RecentLoginUrl.now(_urlText!); - log('LoginController::_saveRecentLoginUrl(): $recentLoginUrl'); - _saveLoginUrlOnMobileInteractor.execute(recentLoginUrl); - } + void _storeBaseUrlToCache(Uri uri) { + log('LoginController::_storeBaseUrlToCache:uri: $uri'); + _saveLoginUrlOnMobileInteractor.execute(RecentLoginUrl.now(uri.toString())); } Future> getAllRecentLoginUrlAction(String pattern) async { @@ -360,19 +373,19 @@ class LoginController extends ReloadableController { )); } - void setUsername(String username) { - log('LoginController::formatUsername(): $username'); - usernameInputController.text = username; - setUserNameText(usernameInputController.text); + void selectUsernameFromSuggestion(RecentLoginUsername recentLoginUsername) { + log('LoginController::selectUsernameFromSuggestion():recentLoginUsername: $recentLoginUsername'); + usernameInputController.text = recentLoginUsername.username; + _username = UserName(recentLoginUsername.username); + + if (loginFormType.value == LoginFormType.credentialForm) { + passFocusNode.requestFocus(); + } } - void _saveRecentLoginUsername() { - if(PlatformInfo.isWeb || _userNameText == null || _userNameText!.isEmpty || !_userNameText!.isEmail) { - return ; - } - final recentLoginUsername = RecentLoginUsername.now(_userNameText!); - log('LoginController::_saveRecentLoginUsername(): $recentLoginUsername'); - _saveLoginUsernameOnMobileInteractor.execute(recentLoginUsername); + void _storeUsernameToCache(String userName) { + log('LoginController::_storeUsername():userName: $userName'); + _saveLoginUsernameOnMobileInteractor.execute(RecentLoginUsername.now(userName)); } Future> getAllRecentLoginUsernameAction(String pattern) async { @@ -386,11 +399,82 @@ class LoginController extends ReloadableController { )); } - Uri? _getBaseUrl() => PlatformInfo.isWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); + Uri? get _currentBaseUrl => PlatformInfo.isWeb + ? Uri.tryParse(AppConfig.baseUrl) + : _baseUri; + + void invokeDNSLookupToGetJmapUrl() { + log('LoginController::invokeDNSLookupToGetJmapUrl:_username $_username'); + if (_username == null) { + consumeState(Stream.value(Left(AuthenticationUserFailure(CanNotFoundUserName())))); + } else { + if (_username!.value.isEmail) { + _storeUsernameToCache(_username!.value); + consumeState(_dnsLookupToGetJmapUrlInteractor.execute(_username!.value)); + } else { + _username = null; + _clearTextInputField(); + _showBaseUrlForm(); + } + } + } + + void _handleDNSLookupToGetJmapUrlSuccess(DNSLookupToGetJmapUrlSuccess success) { + onBaseUrlChange(success.jmapUrl); + _checkOIDCIsAvailable(); + } + + void _showBaseUrlForm() { + clearState(); + loginFormType.value = LoginFormType.baseUrlForm; + baseUrlFocusNode.requestFocus(); + } + + void _showPasswordForm() { + clearState(); + loginFormType.value = LoginFormType.passwordForm; + passFocusNode.requestFocus(); + } + + void onUsernameChange(String value) { + if (value.isEmpty) { + _username = null; + } else { + _username = UserName(value); + } + } + + void onPasswordChange(String value) { + if (value.isEmpty) { + _password = null; + } else { + _password = Password(value); + } + } + + void onBaseUrlChange(String value) { + if (value.isEmpty) { + _baseUri = null; + } else { + if (value.isValid()) { + log('LoginController::onBaseUrlChange:value: $value'); + urlInputController.text = value.removePrefix(); + } + _baseUri = Uri.tryParse(value.formatURLValid()); + } + } + + void _clearTextInputField() { + urlInputController.clear(); + usernameInputController.clear(); + passwordInputController.clear(); + } @override void onClose() { passFocusNode.dispose(); + baseUrlFocusNode.dispose(); + userNameFocusNode.dispose(); urlInputController.dispose(); usernameInputController.dispose(); passwordInputController.dispose(); diff --git a/lib/features/login/presentation/login_form_type.dart b/lib/features/login/presentation/login_form_type.dart index dd8804698..53f820737 100644 --- a/lib/features/login/presentation/login_form_type.dart +++ b/lib/features/login/presentation/login_form_type.dart @@ -1,3 +1,7 @@ enum LoginFormType { - none, baseUrlForm, credentialForm, ssoForm + none, + baseUrlForm, + credentialForm, + passwordForm, + dnsLookupForm; } \ 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 47813724a..75cea1029 100644 --- a/lib/features/login/presentation/login_view.dart +++ b/lib/features/login/presentation/login_view.dart @@ -3,7 +3,6 @@ import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/widget/recent_item_tile_widget.dart'; @@ -11,7 +10,12 @@ import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart' import 'package:tmail_ui_user/features/login/presentation/base_login_view.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/privacy_link_widget.dart'; +import 'package:tmail_ui_user/features/login/presentation/widgets/dns_lookup_input_form.dart'; +import 'package:tmail_ui_user/features/login/presentation/widgets/horizontal_progress_loading_button.dart'; +import 'package:tmail_ui_user/features/login/presentation/widgets/login_back_button.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_message_widget.dart'; +import 'package:tmail_ui_user/features/login/presentation/widgets/password_input_form.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class LoginView extends BaseLoginView { @@ -31,13 +35,16 @@ class LoginView extends BaseLoginView { child: SafeArea( child: _supportScrollForm(context) ? Stack(children: [ - Center(child: SingleChildScrollView( + Center( + child: SingleChildScrollView( scrollDirection: Axis.vertical, - child: _buildCenterForm(context))), + child: _buildCenterForm(context) + ) + ), Obx(() { - if (controller.loginFormType.value == LoginFormType.credentialForm - || controller.loginFormType.value == LoginFormType.ssoForm) { - return _buildBackButton(context); + if (controller.loginFormType.value == LoginFormType.passwordForm || + controller.loginFormType.value == LoginFormType.credentialForm) { + return LoginBackButton(onBackAction: controller.handleBackButtonAction); } return const SizedBox.shrink(); }) @@ -45,9 +52,9 @@ class LoginView extends BaseLoginView { : Stack(children: [ _buildCenterForm(context), Obx(() { - if (controller.loginFormType.value == LoginFormType.credentialForm - || controller.loginFormType.value == LoginFormType.ssoForm) { - return _buildBackButton(context); + if (controller.loginFormType.value == LoginFormType.passwordForm || + controller.loginFormType.value == LoginFormType.credentialForm) { + return LoginBackButton(onBackAction: controller.handleBackButtonAction); } return const SizedBox.shrink(); }) @@ -75,9 +82,28 @@ class LoginView extends BaseLoginView { style: const TextStyle(fontSize: 32, color: AppColor.colorNameEmail, fontWeight: FontWeight.w900) ) ), - Obx(() => buildLoginMessage(context, controller.viewState.value)), + Obx(() => LoginMessageWidget( + formType: controller.loginFormType.value, + viewState: controller.viewState.value, + )), Obx(() { switch (controller.loginFormType.value) { + case LoginFormType.dnsLookupForm: + return DNSLookupInputForm( + textEditingController: controller.usernameInputController, + onTextChange: controller.onUsernameChange, + onTextSubmitted: (_) => controller.invokeDNSLookupToGetJmapUrl(), + suggestionsCallback: controller.getAllRecentLoginUsernameAction, + onSuggestionSelected: controller.selectUsernameFromSuggestion + ); + case LoginFormType.passwordForm: + return PasswordInputForm( + key: const Key('password_input_form'), + textEditingController: controller.passwordInputController, + focusNode: controller.passFocusNode, + onTextChange: controller.onPasswordChange, + onTextSubmitted: (_) => controller.handleLoginPressed(), + ); case LoginFormType.baseUrlForm: return _buildUrlInput(context); case LoginFormType.credentialForm: @@ -97,22 +123,6 @@ class LoginView extends BaseLoginView { ); } - 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( - controller.imagePaths.icBack, - alignment: Alignment.center, - colorFilter: AppColor.primaryColor.asFilter() - ) - ), - ); - } - Widget _buildUrlInput(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 24, left: 24, bottom: 24), @@ -120,19 +130,17 @@ class LoginView extends BaseLoginView { controller: controller.urlInputController, textInputAction: TextInputAction.next, keyboardType: TextInputType.url, + focusNode: controller.baseUrlFocusNode, + onTextChange: controller.onBaseUrlChange, onTextSubmitted: (value) => controller.handleNextInUrlInputFormPress(), - onTextChange: controller.formatUrl, decoration: (LoginInputDecorationBuilder() ..setLabelText(AppLocalizations.of(context).prefix_https) ..setPrefixText(AppLocalizations.of(context).prefix_https)) .build(), debounceDuration: const Duration(milliseconds: 300), - suggestionsCallback: (pattern) async { - controller.formatUrl(pattern); - return controller.getAllRecentLoginUrlAction(pattern); - }, - itemBuilder: (context, loginUrl) => RecentItemTileWidget(loginUrl, imagePath: controller.imagePaths), - onSuggestionSelected: (loginUrl) => controller.formatUrl(loginUrl.url), + suggestionsCallback: controller.getAllRecentLoginUrlAction, + itemBuilder: (_, loginUrl) => RecentItemTileWidget(loginUrl), + onSuggestionSelected: (loginUrl) => controller.selectBaseUrlFromSuggestion(loginUrl.url), suggestionsBoxDecoration: const SuggestionsBoxDecoration(borderRadius: BorderRadius.all(Radius.circular(14))), noItemsFoundBuilder: (context) => const SizedBox(), hideOnEmpty: true, @@ -156,20 +164,23 @@ class LoginView extends BaseLoginView { width: controller.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( + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: AppColor.primaryColor, + shape: 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: () { - controller.handleNextInUrlInputFormPress(); + if (controller.loginFormType.value == LoginFormType.dnsLookupForm) { + controller.invokeDNSLookupToGetJmapUrl(); + } else { + controller.handleNextInUrlInputFormPress(); + } } ) ); @@ -195,27 +206,29 @@ class LoginView extends BaseLoginView { return Obx(() => controller.viewState.value.fold( (failure) { switch (controller.loginFormType.value) { + case LoginFormType.dnsLookupForm: case LoginFormType.baseUrlForm: return _buildNextButtonInContext(context); + case LoginFormType.passwordForm: case LoginFormType.credentialForm: return _buildLoginButtonInContext(context); - case LoginFormType.ssoForm: - return _buildLoginButtonInContext(context); default: return const SizedBox.shrink(); } }, (success) { if (success is LoadingState) { - return buildLoadingCircularProgress(); + return _supportScrollForm(context) + ? const HorizontalProgressLoadingButton() + : _buildExpandedButton(context, const HorizontalProgressLoadingButton()); } else { switch (controller.loginFormType.value) { + case LoginFormType.dnsLookupForm: case LoginFormType.baseUrlForm: return _buildNextButtonInContext(context); + case LoginFormType.passwordForm: case LoginFormType.credentialForm: return _buildLoginButtonInContext(context); - case LoginFormType.ssoForm: - return _buildLoginButtonInContext(context); default: return const SizedBox.shrink(); } diff --git a/lib/features/login/presentation/login_view_web.dart b/lib/features/login/presentation/login_view_web.dart index 36cf2f84b..9db8e30b9 100644 --- a/lib/features/login/presentation/login_view_web.dart +++ b/lib/features/login/presentation/login_view_web.dart @@ -8,6 +8,7 @@ import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:tmail_ui_user/features/login/presentation/base_login_view.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/privacy_link_widget.dart'; +import 'package:tmail_ui_user/features/login/presentation/widgets/login_message_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class LoginView extends BaseLoginView { @@ -47,13 +48,14 @@ class LoginView extends BaseLoginView { style: const TextStyle(fontSize: 32, color: AppColor.colorNameEmail, fontWeight: FontWeight.w900) ) ), - Obx(() => buildLoginMessage(context, controller.viewState.value)), + Obx(() => LoginMessageWidget( + formType: controller.loginFormType.value, + viewState: controller.viewState.value, + )), Obx(() { switch (controller.loginFormType.value) { case LoginFormType.credentialForm: return buildInputCredentialForm(context); - case LoginFormType.ssoForm: - return const SizedBox(height: 150); default: return const SizedBox.shrink(); } @@ -179,13 +181,14 @@ class LoginView extends BaseLoginView { style: const TextStyle(fontSize: 32, color: AppColor.colorNameEmail, fontWeight: FontWeight.w900) ) ), - Obx(() => buildLoginMessage(context, controller.viewState.value)), + Obx(() => LoginMessageWidget( + formType: controller.loginFormType.value, + viewState: controller.viewState.value, + )), Obx(() { switch (controller.loginFormType.value) { case LoginFormType.credentialForm: return buildInputCredentialForm(context); - case LoginFormType.ssoForm: - return const SizedBox(height: 150); default: return const SizedBox.shrink(); } @@ -232,41 +235,12 @@ class LoginView extends BaseLoginView { ); } - Widget _buildSSOButton(BuildContext context) { - return Container( - margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24), - width: controller.responsiveUtils.getDeviceWidth(context),height: 48, - child: ElevatedButton( - key: const Key('ssoSubmitForm'), - 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).singleSignOn, - style: const TextStyle(fontSize: 16, color: Colors.white) - ), - onPressed: () { - controller.handleSSOPressed(); - } - ) - ); - } - Widget _buildLoadingProgress(BuildContext context) { return Obx(() => controller.viewState.value.fold( (failure) { switch (controller.loginFormType.value) { - case LoginFormType.baseUrlForm: - return const SizedBox.shrink(); case LoginFormType.credentialForm: return buildLoginButton(context); - case LoginFormType.ssoForm: - return _buildSSOButton(context); default: return const SizedBox.shrink(); } @@ -276,12 +250,8 @@ class LoginView extends BaseLoginView { return buildLoadingCircularProgress(); } else { switch (controller.loginFormType.value) { - case LoginFormType.baseUrlForm: - return const SizedBox.shrink(); case LoginFormType.credentialForm: return buildLoginButton(context); - case LoginFormType.ssoForm: - return _buildSSOButton(context); default: return const SizedBox.shrink(); } diff --git a/lib/features/login/presentation/model/login_arguments.dart b/lib/features/login/presentation/model/login_arguments.dart index b87eb6b9f..55644c402 100644 --- a/lib/features/login/presentation/model/login_arguments.dart +++ b/lib/features/login/presentation/model/login_arguments.dart @@ -5,9 +5,13 @@ import 'package:tmail_ui_user/main/routes/router_arguments.dart'; class LoginArguments extends RouterArguments { final LoginFormType loginFormType; + final String? baseUrl; - LoginArguments(this.loginFormType); + LoginArguments(this.loginFormType, {this.baseUrl}); @override - List get props => [loginFormType]; + List get props => [ + loginFormType, + baseUrl + ]; } \ No newline at end of file diff --git a/lib/features/login/presentation/widgets/dns_lookup_input_form.dart b/lib/features/login/presentation/widgets/dns_lookup_input_form.dart new file mode 100644 index 000000000..e825d021f --- /dev/null +++ b/lib/features/login/presentation/widgets/dns_lookup_input_form.dart @@ -0,0 +1,57 @@ + +import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:tmail_ui_user/features/base/widget/recent_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/login/domain/model/recent_login_username.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 DNSLookupInputForm extends StatelessWidget { + + final TextEditingController textEditingController; + final ValueChanged onTextChange; + final ValueChanged onTextSubmitted; + final SuggestionsCallback suggestionsCallback; + final SuggestionSelectionCallback onSuggestionSelected; + + const DNSLookupInputForm({ + super.key, + required this.textEditingController, + required this.onTextChange, + required this.onTextSubmitted, + required this.suggestionsCallback, + required this.onSuggestionSelected + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 16, start: 24, end: 24), + child: TypeAheadFormFieldBuilder( + controller: textEditingController, + onTextChange: onTextChange, + onTextSubmitted: onTextSubmitted, + textInputAction: TextInputAction.next, + autocorrect: false, + autofillHints: const [AutofillHints.email], + keyboardType: TextInputType.emailAddress, + decoration: (LoginInputDecorationBuilder() + ..setLabelText(AppLocalizations.of(context).email) + ..setHintText(AppLocalizations.of(context).email) + ).build(), + debounceDuration: const Duration(milliseconds: 300), + suggestionsCallback: suggestionsCallback, + itemBuilder: (_, loginUsername) => RecentItemTileWidget(loginUsername), + onSuggestionSelected: onSuggestionSelected, + suggestionsBoxDecoration: const SuggestionsBoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(14)) + ), + noItemsFoundBuilder: (context) => const SizedBox(), + hideOnEmpty: true, + hideOnError: true, + hideOnLoading: true, + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/login/presentation/widgets/horizontal_progress_loading_button.dart b/lib/features/login/presentation/widgets/horizontal_progress_loading_button.dart new file mode 100644 index 000000000..dc6e9f737 --- /dev/null +++ b/lib/features/login/presentation/widgets/horizontal_progress_loading_button.dart @@ -0,0 +1,30 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class HorizontalProgressLoadingButton extends StatelessWidget { + const HorizontalProgressLoadingButton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsetsDirectional.only(bottom: 16, start: 24, end: 24), + width: context.width, + height: 48, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.primaryColor, + disabledBackgroundColor: AppColor.primaryColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ) + ), + onPressed: null, + child: const LinearProgressIndicator( + color: Colors.white, + backgroundColor: AppColor.primaryColor + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/login/presentation/widgets/login_back_button.dart b/lib/features/login/presentation/widgets/login_back_button.dart new file mode 100644 index 000000000..593379102 --- /dev/null +++ b/lib/features/login/presentation/widgets/login_back_button.dart @@ -0,0 +1,30 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +class LoginBackButton extends StatelessWidget { + + final _imagePath = Get.find(); + final VoidCallback onBackAction; + + LoginBackButton({super.key, required this.onBackAction}); + + @override + Widget build(BuildContext context) { + return PositionedDirectional( + start: 12, + top: 12, + child: IconButton( + onPressed: onBackAction, + icon: SvgPicture.asset( + _imagePath.icBack, + alignment: Alignment.center, + colorFilter: AppColor.primaryColor.asFilter() + ) + ), + ); + } +} diff --git a/lib/features/login/presentation/widgets/login_message_widget.dart b/lib/features/login/presentation/widgets/login_message_widget.dart new file mode 100644 index 000000000..a51b72ba5 --- /dev/null +++ b/lib/features/login/presentation/widgets/login_message_widget.dart @@ -0,0 +1,83 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/login/domain/state/dns_lookup_to_get_jmap_url_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_oidc_configuration_state.dart'; +import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/message_toast_utils.dart'; + +class LoginMessageWidget extends StatelessWidget { + + static const double _loginTextFieldWidthSmallScreen = 280.0; + static const double _loginTextFieldWidthLargeScreen = 320.0; + + final LoginFormType formType; + final Either viewState; + + const LoginMessageWidget({ + super.key, + required this.formType, + required this.viewState + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsetsDirectional.only( + top: 11, + bottom: 36, + start: 58, + end: 58 + ), + child: SizedBox( + width: context.width < ResponsiveUtils.minTabletWidth + ? _loginTextFieldWidthSmallScreen + : _loginTextFieldWidthLargeScreen, + child: Text( + viewState.fold( + (failure) { + if (failure is GetOIDCConfigurationFailure) { + return AppLocalizations.of(context).canNotVerifySSOConfiguration; + } else if (failure is DNSLookupToGetJmapUrlFailure) { + return AppLocalizations.of(context).dnsLookupLoginMessage; + } else if (failure is FeatureFailure) { + final errorMessage = MessageToastUtils.getMessageByException(context, failure.exception); + return errorMessage ?? AppLocalizations.of(context).unknownError; + } else { + return AppLocalizations.of(context).unknownError; + } + }, + (success) { + if (formType == LoginFormType.credentialForm) { + return AppLocalizations.of(context).loginInputCredentialMessage; + } else if (formType == LoginFormType.dnsLookupForm) { + return AppLocalizations.of(context).dnsLookupLoginMessage; + } else if (formType == LoginFormType.passwordForm) { + return AppLocalizations.of(context).enterYourPasswordToSignIn; + } else if (formType == LoginFormType.baseUrlForm) { + return AppLocalizations.of(context).loginInputUrlMessage; + } else { + return ''; + } + } + ), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + color: viewState.fold( + (failure) => AppColor.textFieldErrorBorderColor, + (success) => AppColor.colorNameEmail + ) + ), + ) + ) + ); + } +} diff --git a/lib/features/login/presentation/widgets/password_input_form.dart b/lib/features/login/presentation/widgets/password_input_form.dart new file mode 100644 index 000000000..e89b77b67 --- /dev/null +++ b/lib/features/login/presentation/widgets/password_input_form.dart @@ -0,0 +1,35 @@ + +import 'package:flutter/material.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 PasswordInputForm extends StatelessWidget { + final TextEditingController textEditingController; + final FocusNode focusNode; + final ValueChanged onTextChange; + final ValueChanged onTextSubmitted; + + const PasswordInputForm({ + super.key, + required this.textEditingController, + required this.focusNode, + required this.onTextChange, + required this.onTextSubmitted + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 16, start: 24, end: 24), + child: LoginTextInputBuilder( + controller: textEditingController, + autofillHints: const [AutofillHints.password], + textInputAction: TextInputAction.done, + hintText: AppLocalizations.of(context).password, + focusNode: focusNode, + onTextChange: onTextChange, + onSubmitted: onTextSubmitted, + ), + ); + } +} diff --git a/lib/features/mailbox/domain/state/create_default_mailbox_state.dart b/lib/features/mailbox/domain/state/create_default_mailbox_state.dart index 3db23d0d5..39f4c8a92 100644 --- a/lib/features/mailbox/domain/state/create_default_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/create_default_mailbox_state.dart @@ -16,8 +16,10 @@ class CreateDefaultMailboxAllSuccess extends UIActionState { } class CreateDefaultMailboxFailure extends FeatureFailure { - CreateDefaultMailboxFailure(dynamic exception) : super(exception: exception); + final jmap.State? currentMailboxState; + + CreateDefaultMailboxFailure(this.currentMailboxState, dynamic exception) : super(exception: exception); @override - List get props => [exception]; + List get props => [currentMailboxState, exception]; } \ No newline at end of file diff --git a/lib/features/mailbox/domain/usecases/create_new_default_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/create_new_default_mailbox_interactor.dart index 86cf1033f..dce6e6297 100644 --- a/lib/features/mailbox/domain/usecases/create_new_default_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/create_new_default_mailbox_interactor.dart @@ -1,9 +1,10 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:dartz/dartz.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/create_default_mailbox_state.dart'; @@ -13,30 +14,37 @@ class CreateDefaultMailboxInteractor { CreateDefaultMailboxInteractor(this._mailboxRepository); - Stream> execute( + Stream> execute( Session session, AccountId accountId, List listRole ) async* { + final currentMailboxState = await _getCurrentMailboxState(session, accountId); try { - yield Right(CreateDefaultMailboxLoading()); - - final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); - + yield dartz.Right(CreateDefaultMailboxLoading()); final listMailboxCreated = await _mailboxRepository.createDefaultMailbox( session, accountId, listRole ); - log('CreateDefaultMailboxInteractor::execute:listMailboxCreated: ${listMailboxCreated.length}'); await _mailboxRepository.setRoleDefaultMailbox( session, accountId, listMailboxCreated ); - yield Right(CreateDefaultMailboxAllSuccess(currentMailboxState: currentMailboxState)); + yield dartz.Right(CreateDefaultMailboxAllSuccess(currentMailboxState: currentMailboxState)); } catch (e) { - yield Left(CreateDefaultMailboxFailure(e)); + yield dartz.Left(CreateDefaultMailboxFailure(currentMailboxState, e)); + } + } + + Future _getCurrentMailboxState(Session session, AccountId accountId) async { + try { + final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); + log('CreateDefaultMailboxInteractor::_getCurrentMailboxState:currentMailboxState: $currentMailboxState'); + return currentMailboxState; + } catch (e) { + return null; } } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index de3f1405a..fd312b2d5 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -193,7 +193,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } else if (failure is RefreshChangesAllMailboxFailure) { _clearNewFolderId(); } else if (failure is CreateDefaultMailboxFailure) { - refreshAllMailbox(); + _refreshMailboxChanges(currentMailboxState: failure.currentMailboxState); } } diff --git a/lib/features/mailbox/presentation/model/mailbox_tree.dart b/lib/features/mailbox/presentation/model/mailbox_tree.dart index fd4dbf766..125e6e3c6 100644 --- a/lib/features/mailbox/presentation/model/mailbox_tree.dart +++ b/lib/features/mailbox/presentation/model/mailbox_tree.dart @@ -127,9 +127,7 @@ class MailboxTree with EquatableMixin { } Map get mapPresentationMailboxByRole { - if (root.childrenItems?.isEmpty == true) { - return {}; - } else { + if (root.childrenItems?.isNotEmpty == true) { final listPresentationMailboxHasRole = root.childrenItems! .where((node) => node.item.role != null) .map((node) => node.item) @@ -139,6 +137,8 @@ class MailboxTree with EquatableMixin { for (var mailbox in listPresentationMailboxHasRole) mailbox.role!: mailbox }; + } else { + return {}; } } diff --git a/lib/features/mailbox_dashboard/data/network/spam_report_api.dart b/lib/features/mailbox_dashboard/data/network/spam_report_api.dart index 3c0244b79..06caecadb 100644 --- a/lib/features/mailbox_dashboard/data/network/spam_report_api.dart +++ b/lib/features/mailbox_dashboard/data/network/spam_report_api.dart @@ -8,6 +8,7 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/get/get_mailbox_method.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/get/get_mailbox_response.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/query/query_mailbox_method.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spam_emails_response.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; @@ -50,11 +51,10 @@ class SpamReportApi { final mailboxResponse = result .parse(getMailboxInvocation.methodCallId, GetMailboxResponse.deserialize); - return Future.sync(() async { - final unreadSpamMailbox = mailboxResponse?.list.first; - return UnreadSpamEmailsResponse(unreadSpamMailbox: unreadSpamMailbox); - }).catchError((error) { - throw error; - }); + if (mailboxResponse?.list.isNotEmpty == true) { + return UnreadSpamEmailsResponse(unreadSpamMailbox: mailboxResponse!.list.first); + } else { + throw NotFoundSpamMailboxException(); + } } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart b/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart index 81401e11a..21917bc85 100644 --- a/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart +++ b/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart @@ -1,4 +1,6 @@ class NotFoundLastTimeDismissedSpamReportException implements Exception {} -class NotFoundSpamMailboxCachedException implements Exception {} \ No newline at end of file +class NotFoundSpamMailboxCachedException implements Exception {} + +class NotFoundSpamMailboxException implements Exception {} \ No newline at end of file diff --git a/lib/features/session/presentation/model/session_arguments.dart b/lib/features/session/presentation/model/session_arguments.dart new file mode 100644 index 000000000..2d624c9c7 --- /dev/null +++ b/lib/features/session/presentation/model/session_arguments.dart @@ -0,0 +1,20 @@ + +import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; +import 'package:tmail_ui_user/main/routes/router_arguments.dart'; + +class SessionArguments extends RouterArguments { + + final LoginFormType loginFormType; + final String baseUrl; + + SessionArguments( + this.loginFormType, + this.baseUrl + ); + + @override + List get props => [ + loginFormType, + baseUrl, + ]; +} \ No newline at end of file diff --git a/lib/features/session/presentation/session_controller.dart b/lib/features/session/presentation/session_controller.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 83ca07bd1..e4f205daa 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-11-14T16:28:55.718679", + "@@last_modified": "2023-11-10T16:53:43.713688", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3481,5 +3481,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "dnsLookupLoginMessage": "To login and access your message please enter your email", + "@dnsLookupLoginMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "enterYourPasswordToSignIn": "Enter your password to sign in", + "@enterYourPasswordToSignIn": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 4b037d864..653aa29fb 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -18,6 +18,7 @@ import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/data/network/config/time_out_interceptors.dart'; +import 'package:tmail_ui_user/features/login/data/network/dns_service.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; import 'package:tmail_ui_user/features/login/data/utils/library_platform/app_auth_plugin/app_auth_plugin.dart'; import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_api.dart'; @@ -41,6 +42,7 @@ class NetworkBindings extends Bindings { _bindingDio(); _bindingApi(); _bindingTransformer(); + _bindingServices(); _bindingException(); } @@ -119,4 +121,8 @@ class NetworkBindings extends Bindings { Get.put(HtmlTransform(Get.find(), Get.find())); Get.put(HtmlAnalyzer(Get.find())); } + + void _bindingServices() { + Get.put(DNSService()); + } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 1e7128c92..7af9d77f4 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3610,4 +3610,18 @@ class AppLocalizations { name: 'notFoundSession', ); } + + String get dnsLookupLoginMessage { + return Intl.message( + 'To login and access your message please enter your email', + name: 'dnsLookupLoginMessage' + ); + } + + String get enterYourPasswordToSignIn { + return Intl.message( + 'Enter your password to sign in', + name: 'enterYourPasswordToSignIn' + ); + } } \ No newline at end of file diff --git a/lib/main/utils/message_toast_utils.dart b/lib/main/utils/message_toast_utils.dart new file mode 100644 index 000000000..9344da538 --- /dev/null +++ b/lib/main/utils/message_toast_utils.dart @@ -0,0 +1,36 @@ + +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class MessageToastUtils { + + static String? getMessageByException(BuildContext context, dynamic exception) { + if (exception is CanNotFoundBaseUrl) { + return AppLocalizations.of(context).requiredUrl; + } else if (exception is CanNotFoundUserName) { + return AppLocalizations.of(context).requiredEmail; + } else if (exception is CanNotFoundPassword) { + return AppLocalizations.of(context).requiredPassword; + } else if (exception is CanNotFoundOIDCLinks) { + return AppLocalizations.of(context).ssoNotAvailable; + } else if (exception is CanNotFoundToken) { + return AppLocalizations.of(context).canNotGetToken; + } else if (exception is ConnectionTimeout || exception is BadGateway || exception is SocketError) { + return AppLocalizations.of(context).wrongUrlMessage; + } else if (exception is BadCredentialsException) { + return AppLocalizations.of(context).badCredentials; + } else if (exception is ConnectionError) { + return AppLocalizations.of(context).connectionError; + } else if (exception is UnknownError && exception.message != null) { + return '[${exception.code ?? ''}] ${exception.message}'; + } else if (exception is NotFoundSessionException) { + return AppLocalizations.of(context).notFoundSession; + } else { + return null; + } + } +}