diff --git a/lib/features/login/data/network/oidc_error.dart b/lib/features/login/data/network/oidc_error.dart index b18abf6c83..c529a25907 100644 --- a/lib/features/login/data/network/oidc_error.dart +++ b/lib/features/login/data/network/oidc_error.dart @@ -2,4 +2,6 @@ class CanNotFoundOIDCAuthority implements Exception {} class CanNotFoundOIDCLinks implements Exception {} -class CanNotFoundToken implements Exception {} \ No newline at end of file +class CanNotFindToken implements Exception {} + +class CanRetryOIDCException implements Exception {} \ No newline at end of file diff --git a/lib/features/login/data/network/oidc_http_client.dart b/lib/features/login/data/network/oidc_http_client.dart index 808940c682..214a75594e 100644 --- a/lib/features/login/data/network/oidc_http_client.dart +++ b/lib/features/login/data/network/oidc_http_client.dart @@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.d import 'package:tmail_ui_user/features/login/data/network/endpoint.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; +import 'package:dio/dio.dart' show DioError; class OIDCHttpClient { @@ -21,24 +22,29 @@ class OIDCHttpClient { OIDCHttpClient(this._dioClient); Future checkOIDCIsAvailable(OIDCRequest oidcRequest) async { - final result = await _dioClient.get( + try { + final result = await _dioClient.get( Endpoint.webFinger - .generateOIDCPath(Uri.parse(oidcRequest.baseUrl)) - .withQueryParameters([ - StringQueryParameter('resource', oidcRequest.resourceUrl), - StringQueryParameter('rel', OIDCRequest.relUrl), - ]) - .generateEndpointPath() - ); - log('OIDCHttpClient::checkOIDCIsAvailable(): RESULT: $result'); - if (result != null) { + .generateOIDCPath(Uri.parse(oidcRequest.baseUrl)) + .withQueryParameters([ + StringQueryParameter('resource', oidcRequest.resourceUrl), + StringQueryParameter('rel', OIDCRequest.relUrl), + ]) + .generateEndpointPath() + ); + log('OIDCHttpClient::checkOIDCIsAvailable(): RESULT: $result'); if (result is Map) { return OIDCResponse.fromJson(result); } else { return OIDCResponse.fromJson(jsonDecode(result)); } - } else { - throw CanNotFoundOIDCLinks(); + } on DioError catch (exception) { + if (exception.response?.statusCode == 404) { + throw CanNotFoundOIDCLinks(); + } + throw CanRetryOIDCException(); + } catch (_) { + throw CanRetryOIDCException(); } } diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index fb2c449860..425e0f0b6e 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -125,8 +125,9 @@ class LoginController extends ReloadableController { log('LoginController::handleFailureViewState(): $failure'); if (failure is GetAuthenticationInfoFailure) { getAuthenticatedAccountAction(); - } else if (failure is CheckOIDCIsAvailableFailure || - failure is GetStoredOidcConfigurationFailure || + } else if (failure is CheckOIDCIsAvailableFailure) { + _handleCheckOIDCIsAvailableFailure(failure); + } else if (failure is GetStoredOidcConfigurationFailure || failure is GetOIDCIsAvailableFailure || failure is GetOIDCConfigurationFailure ) { @@ -174,8 +175,9 @@ class LoginController extends ReloadableController { @override void handleUrgentException({Failure? failure, Exception? exception}) { logError('LoginController::handleUrgentException:Exception: $exception | Failure: $failure'); - if (failure is CheckOIDCIsAvailableFailure || - failure is GetStoredOidcConfigurationFailure || + if (failure is CheckOIDCIsAvailableFailure) { + _handleCheckOIDCIsAvailableFailure(failure); + } else if (failure is GetStoredOidcConfigurationFailure || failure is GetOIDCConfigurationFailure || failure is GetOIDCIsAvailableFailure) { _handleCommonOIDCFailure(); @@ -197,6 +199,23 @@ class LoginController extends ReloadableController { ); } + void _handleCheckOIDCIsAvailableFailure(CheckOIDCIsAvailableFailure failure) { + if (failure.exception is CanNotFoundOIDCLinks) { + _handleCommonOIDCFailure(); + } else { + loginFormType.value = LoginFormType.retry; + } + } + + void retryCheckOidc() { + if (PlatformInfo.isMobile) { + loginFormType.value = LoginFormType.dnsLookupForm; + } else { + loginFormType.value = LoginFormType.none; + } + _checkOIDCIsAvailable(); + } + void _getAuthenticationInfo() { consumeState(_getAuthenticationInfoInteractor.execute()); } @@ -453,6 +472,9 @@ class LoginController extends ReloadableController { } else { _username = UserName(value); } + if (loginFormType.value == LoginFormType.retry && PlatformInfo.isMobile) { + loginFormType.value = LoginFormType.dnsLookupForm; + } } void onPasswordChange(String value) { diff --git a/lib/features/login/presentation/login_form_type.dart b/lib/features/login/presentation/login_form_type.dart index 53f820737c..ffcc316669 100644 --- a/lib/features/login/presentation/login_form_type.dart +++ b/lib/features/login/presentation/login_form_type.dart @@ -1,5 +1,6 @@ enum LoginFormType { none, + retry, baseUrlForm, credentialForm, passwordForm, diff --git a/lib/features/login/presentation/login_view.dart b/lib/features/login/presentation/login_view.dart index 5e028d818d..28592482e7 100644 --- a/lib/features/login/presentation/login_view.dart +++ b/lib/features/login/presentation/login_view.dart @@ -88,6 +88,7 @@ class LoginView extends BaseLoginView { Obx(() { switch (controller.loginFormType.value) { case LoginFormType.dnsLookupForm: + case LoginFormType.retry: return DNSLookupInputForm( textEditingController: controller.usernameInputController, onTextChange: controller.onUsernameChange, @@ -174,6 +175,9 @@ class LoginView extends BaseLoginView { style: const TextStyle(fontSize: 16, color: Colors.white) ), onPressed: () { + if (controller.loginFormType.value == LoginFormType.retry) { + controller.loginFormType.value = LoginFormType.dnsLookupForm; + } if (controller.loginFormType.value == LoginFormType.dnsLookupForm) { controller.invokeDNSLookupToGetJmapUrl(); } else { @@ -206,6 +210,7 @@ class LoginView extends BaseLoginView { switch (controller.loginFormType.value) { case LoginFormType.dnsLookupForm: case LoginFormType.baseUrlForm: + case LoginFormType.retry: return _buildNextButtonInContext(context); case LoginFormType.passwordForm: case LoginFormType.credentialForm: diff --git a/lib/features/login/presentation/login_view_web.dart b/lib/features/login/presentation/login_view_web.dart index 5bd119f68c..b56034117a 100644 --- a/lib/features/login/presentation/login_view_web.dart +++ b/lib/features/login/presentation/login_view_web.dart @@ -10,6 +10,7 @@ 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/features/login/presentation/widgets/try_again_button.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class LoginView extends BaseLoginView { @@ -57,6 +58,11 @@ class LoginView extends BaseLoginView { switch (controller.loginFormType.value) { case LoginFormType.credentialForm: return buildInputCredentialForm(context); + case LoginFormType.retry: + return TryAgainButton( + onRetry: controller.retryCheckOidc, + responsiveUtils: controller.responsiveUtils, + ); default: return const SizedBox.shrink(); } @@ -199,6 +205,11 @@ class LoginView extends BaseLoginView { switch (controller.loginFormType.value) { case LoginFormType.credentialForm: return buildInputCredentialForm(context); + case LoginFormType.retry: + return TryAgainButton( + onRetry: controller.retryCheckOidc, + responsiveUtils: controller.responsiveUtils, + ); default: return const SizedBox.shrink(); } diff --git a/lib/features/login/presentation/widgets/try_again_button.dart b/lib/features/login/presentation/widgets/try_again_button.dart new file mode 100644 index 0000000000..b8d48be305 --- /dev/null +++ b/lib/features/login/presentation/widgets/try_again_button.dart @@ -0,0 +1,31 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class TryAgainButton extends StatelessWidget { + const TryAgainButton({ + super.key, + required this.onRetry, + required this.responsiveUtils, + }); + + final VoidCallback onRetry; + final ResponsiveUtils responsiveUtils; + + @override + Widget build(BuildContext context) { + return TMailButtonWidget.fromText( + text: AppLocalizations.of(context).tryAgain, + textStyle: const TextStyle(fontSize: 16, color: Colors.white), + backgroundColor: AppColor.primaryColor, + onTapActionCallback: onRetry, + borderRadius: 10, + margin: const EdgeInsetsDirectional.only(bottom: 16, start: 24, end: 24), + width: responsiveUtils.getDeviceWidth(context), + textAlign: TextAlign.center, + padding: const EdgeInsets.symmetric(vertical: 12), + ); + } +} \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 371eb331f4..04cf96ed56 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -4005,5 +4005,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "tryAgain": "Try again", + "@tryAgain": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youAreOffline": "You are offline. It looks like you are not connected.", + "@youAreOffline": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 12b3d2d0f8..44cc48b95d 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4199,4 +4199,18 @@ class AppLocalizations { 'You have not selected any action for the rule.', name: 'youHaveNotSelectedAnyActionForRule'); } + + String get tryAgain { + return Intl.message( + 'Try again', + name: 'tryAgain', + ); + } + + String get youAreOffline { + return Intl.message( + 'You are offline. It looks like you are not connected.', + name: 'youAreOffline', + ); + } } \ No newline at end of file diff --git a/lib/main/utils/toast_manager.dart b/lib/main/utils/toast_manager.dart index c5e8bc06f3..03a6317c9a 100644 --- a/lib/main/utils/toast_manager.dart +++ b/lib/main/utils/toast_manager.dart @@ -28,7 +28,7 @@ class ToastManager { return AppLocalizations.of(context).requiredPassword; } else if (exception is CanNotFoundOIDCLinks) { return AppLocalizations.of(context).ssoNotAvailable; - } else if (exception is CanNotFoundToken) { + } else if (exception is CanNotFindToken) { return AppLocalizations.of(context).canNotGetToken; } else if (exception is ConnectionTimeout || exception is BadGateway || exception is SocketError) { return AppLocalizations.of(context).wrongUrlMessage; @@ -40,6 +40,8 @@ class ToastManager { return '[${exception.code ?? ''}] ${exception.message}'; } else if (exception is NotFoundSessionException) { return AppLocalizations.of(context).notFoundSession; + } else if (exception is NoNetworkError) { + return AppLocalizations.of(context).youAreOffline; } else { return null; } diff --git a/test/features/login/data/network/oidc_http_client_test.dart b/test/features/login/data/network/oidc_http_client_test.dart new file mode 100644 index 0000000000..d60ddd0cec --- /dev/null +++ b/test/features/login/data/network/oidc_http_client_test.dart @@ -0,0 +1,68 @@ +import 'package:core/data/network/dio_client.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:model/oidc/request/oidc_request.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; + +import 'oidc_http_client_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + final dioClient = MockDioClient(); + final oidcHttpClient = OIDCHttpClient(dioClient); + final requestOptions = RequestOptions(); + final oidcRequest = OIDCRequest(baseUrl: '', resourceUrl: ''); + + group('oidc http client test:', () { + test( + 'should throw CanNotFoundOIDCLinks ' + 'when checkOIDCIsAvailable() is called ' + 'and dioClient throw DioError ' + 'and status code is 404', + () { + // arrange + when(dioClient.get(any)).thenThrow(DioError( + requestOptions: requestOptions, + response: Response(requestOptions: requestOptions, statusCode: 404))); + + // assert + expect( + () => oidcHttpClient.checkOIDCIsAvailable(oidcRequest), + throwsA(isA())); + }); + + test( + 'should throw CanRetryOIDCException ' + 'when checkOIDCIsAvailable() is called ' + 'and dioClient throw DioError ' + 'and status code is not 404', + () { + // arrange + when(dioClient.get(any)).thenThrow(DioError( + requestOptions: requestOptions, + response: Response(requestOptions: requestOptions, statusCode: 403))); + + // assert + expect( + () => oidcHttpClient.checkOIDCIsAvailable(oidcRequest), + throwsA(isA())); + }); + + test( + 'should throw CanRetryOIDCException ' + 'when checkOIDCIsAvailable() is called ' + 'and dioClient throw exception that is not DioError', + () { + // arrange + when(dioClient.get(any)).thenThrow(Exception()); + + // assert + expect( + () => oidcHttpClient.checkOIDCIsAvailable(oidcRequest), + throwsA(isA())); + }); + }); +} \ No newline at end of file