TF-2928 Show confirm dialog & handle exception when reload browser or loss of connection

This commit is contained in:
dab246
2024-07-12 18:58:50 +07:00
committed by Dat H. Pham
parent 2a22cb1174
commit eca11b3506
13 changed files with 198 additions and 97 deletions
+1
View File
@@ -19,6 +19,7 @@ export 'presentation/extensions/map_extensions.dart';
export 'domain/exceptions/download_file_exception.dart';
export 'data/extensions/options_extensions.dart';
export 'domain/exceptions/web_session_exception.dart';
export 'domain/exceptions/platform_exception.dart';
// Utils
export 'presentation/utils/theme_utils.dart';
@@ -0,0 +1,14 @@
import 'package:equatable/equatable.dart';
class PlatformException with EquatableMixin implements Exception {
final String message;
PlatformException(this.message);
@override
List<Object> get props => [message];
}
class NoSupportPlatformException extends PlatformException {
NoSupportPlatformException() : super('This platform is not supported');
}
@@ -24,18 +24,19 @@ class ConfirmDialogBuilder {
double? _radiusButton;
EdgeInsetsGeometry? _paddingTitle;
EdgeInsets? _paddingContent;
EdgeInsets? _paddingButton;
EdgeInsetsGeometry? _paddingButton;
EdgeInsets? _marginButton;
EdgeInsets? _outsideDialogPadding;
EdgeInsetsGeometry? _marginIcon;
EdgeInsets? _margin;
double? _widthDialog;
double maxWith;
final double maxWith;
Alignment? _alignment;
Color? _backgroundColor;
bool showAsBottomSheet;
List<TextSpan>? listTextSpan;
int? titleActionButtonMaxLines;
bool isArrangeActionButtonsVertical;
final bool showAsBottomSheet;
final List<TextSpan>? listTextSpan;
final int? titleActionButtonMaxLines;
final bool isArrangeActionButtonsVertical;
OnConfirmButtonAction? _onConfirmButtonAction;
OnCancelButtonAction? _onCancelButtonAction;
@@ -104,10 +105,14 @@ class ConfirmDialogBuilder {
_paddingContent = value;
}
void paddingButton(EdgeInsets? value) {
void paddingButton(EdgeInsetsGeometry? value) {
_paddingButton = value;
}
void marginButton(EdgeInsets? value) {
_marginButton = value;
}
void marginIcon(EdgeInsetsGeometry? value) {
_marginIcon = value;
}
@@ -253,7 +258,7 @@ class ConfirmDialogBuilder {
]
else
Padding(
padding: _paddingButton ?? const EdgeInsetsDirectional.only(bottom: 16, start: 16, end: 16),
padding: _marginButton ?? const EdgeInsetsDirectional.only(bottom: 16, start: 16, end: 16),
child: Row(
children: [
if (_cancelText.isNotEmpty)
@@ -297,7 +302,7 @@ class ConfirmDialogBuilder {
borderRadius: BorderRadius.circular(radius ?? 8),
side: BorderSide(width: 0, color: bgColor ?? AppColor.colorTextButton),
),
padding: const EdgeInsets.all(8),
padding: _paddingButton ?? const EdgeInsets.all(8),
elevation: 0
),
child: Text(
+88 -42
View File
@@ -1,5 +1,8 @@
import 'dart:async';
import 'package:contact/contact/model/capability_contact.dart';
import 'package:core/data/network/config/dynamic_url_interceptors.dart';
import 'package:core/domain/exceptions/platform_exception.dart';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/state/failure.dart';
@@ -60,6 +63,7 @@ import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart';
import 'package:tmail_ui_user/main/utils/app_utils.dart';
import 'package:universal_html/html.dart' as html;
import 'package:uuid/uuid.dart';
abstract class BaseController extends GetxController
@@ -85,9 +89,42 @@ abstract class BaseController extends GetxController
GetStoredFirebaseRegistrationInteractor? _getStoredFirebaseRegistrationInteractor;
DestroyFirebaseRegistrationInteractor? _destroyFirebaseRegistrationInteractor;
StreamSubscription<html.Event>? _subscriptionBrowserOnBeforeUnload;
StreamSubscription<html.Event>? _subscriptionBrowserOnUnload;
final viewState = Rx<Either<Failure, Success>>(Right(UIState.idle));
FpsCallback? fpsCallback;
@override
void onInit() {
super.onInit();
if (PlatformInfo.isWeb) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_triggerBrowserReloadListener();
});
}
}
void _triggerBrowserReloadListener() {
_subscriptionBrowserOnBeforeUnload =
html.window.onBeforeUnload.listen(handleBrowserBeforeReloadAction);
_subscriptionBrowserOnUnload =
html.window.onUnload.listen(handleBrowserReloadAction);
}
Future<void> handleBrowserBeforeReloadAction(html.Event event) async {}
Future<void> handleBrowserReloadAction(html.Event event) async {}
@override
void onClose() {
if (PlatformInfo.isWeb) {
_subscriptionBrowserOnBeforeUnload?.cancel();
_subscriptionBrowserOnUnload?.cancel();
}
super.onClose();
}
void consumeState(Stream<Either<Failure, Success>> newStateStream) async {
newStateStream.listen(onData, onError: onError, onDone: onDone);
}
@@ -105,10 +142,9 @@ abstract class BaseController extends GetxController
viewState.value.fold(
(failure) {
if (failure is FeatureFailure) {
final exception = _performFilterExceptionInError(failure.exception);
logError('$runtimeType::onData:exception: $exception');
if (exception != null) {
handleExceptionAction(failure: failure, exception: exception);
final isUrgentException = _validateUrgentException(failure.exception);
if (isUrgentException) {
handleUrgentException(failure: failure, exception: failure.exception);
} else {
handleFailureViewState(failure);
}
@@ -119,11 +155,11 @@ abstract class BaseController extends GetxController
handleSuccessViewState);
}
void onError(Object error, StackTrace stackTrace) {
logError('$runtimeType::onError():error: $error | stackTrace: $stackTrace');
final exception = _performFilterExceptionInError(error);
if (exception != null) {
handleExceptionAction(exception: exception);
void onError(dynamic error, StackTrace stackTrace) {
logError('$runtimeType::onError():Error: $error | StackTrace: $stackTrace');
final isUrgentException = _validateUrgentException(error);
if (isUrgentException) {
handleUrgentException(exception: error);
} else {
handleErrorViewState(error, stackTrace);
}
@@ -131,10 +167,41 @@ abstract class BaseController extends GetxController
void onDone() {}
Exception? _performFilterExceptionInError(dynamic error) {
logError('$runtimeType::_performFilterExceptionInError(): $error');
if (error is NoNetworkError || error is ConnectionTimeout || error is InternalServerError) {
if (PlatformInfo.isWeb && currentOverlayContext != null && currentContext != null) {
bool _validateUrgentException(dynamic exception) {
return exception is NoNetworkError
|| exception is BadCredentialsException
|| exception is ConnectionError;
}
void handleErrorViewState(Object error, StackTrace stackTrace) {}
void handleUrgentException({Failure? failure, Exception? exception}) {
if (PlatformInfo.isWeb) {
handleUrgentExceptionOnWeb(failure: failure, exception: exception);
} else if (PlatformInfo.isMobile) {
handleUrgentExceptionOnMobile(failure: failure, exception: exception);
} else {
throw NoSupportPlatformException();
}
}
void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) {
logError('$runtimeType::handleUrgentExceptionOnMobile():Failure: $failure | Exception: $exception');
if (exception is ConnectionError) {
if (currentOverlayContext != null && currentContext != null) {
appToast.showToastErrorMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).connectionError);
}
} else if (exception is BadCredentialsException) {
_executeBeforeUnloadAndLogOut();
}
}
void handleUrgentExceptionOnWeb({Failure? failure, Exception? exception}) {
logError('$runtimeType::handleUrgentExceptionOnWeb():Failure: $failure | Exception: $exception');
if (exception is NoNetworkError) {
if (currentOverlayContext != null && currentContext != null) {
appToast.showToastMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).no_internet_connection,
@@ -143,43 +210,22 @@ abstract class BaseController extends GetxController
leadingSVGIcon: imagePaths.icNotConnection,
backgroundColor: AppColor.textFieldErrorBorderColor,
textColor: Colors.white,
infinityToast: true,
);
infinityToast: true,);
}
return error;
} else if (error is BadCredentialsException || error is ConnectionError) {
return error;
}
return null;
}
void handleErrorViewState(Object error, StackTrace stackTrace) {}
void handleExceptionAction({Failure? failure, Exception? exception}) {
logError('$runtimeType::handleExceptionAction():failure: $failure | exception: $exception');
if (exception is ConnectionError) {
} else if (exception is ConnectionError) {
if (currentOverlayContext != null && currentContext != null) {
appToast.showToastErrorMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).connectionError
);
AppLocalizations.of(currentContext!).connectionError);
}
} else if (exception is BadCredentialsException) {
_executeBeforeUnloadAndLogOut();
}
if (!authorizationInterceptors.isAppRunning) {
return;
}
_executeBeforeUnloadAndLogOut(exception);
}
Future<void> _executeBeforeUnloadAndLogOut(Exception? exception) async {
if (exception is BadCredentialsException || exception is ConnectionError) {
if (PlatformInfo.isWeb) {
await executeBeforeUnload();
}
clearDataAndGoToLoginPage();
}
Future<void> _executeBeforeUnloadAndLogOut() async {
await executeBeforeUnload();
clearDataAndGoToLoginPage();
}
void handleFailureViewState(Failure failure) async {
@@ -33,6 +33,7 @@ mixin MessageDialogActionMixin {
Color? actionButtonColor,
Color? cancelButtonColor,
EdgeInsetsGeometry? marginIcon,
EdgeInsetsGeometry? paddingButton,
PopInvokedCallback? onPopInvoked,
bool isArrangeActionButtonsVertical = false,
int? titleActionButtonMaxLines,
@@ -61,8 +62,9 @@ mixin MessageDialogActionMixin {
: const EdgeInsetsDirectional.symmetric(horizontal: 24)
)
..radiusButton(12)
..paddingButton(paddingButton)
..paddingContent(const EdgeInsets.only(left: 24, right: 24, bottom: 24, top: 12))
..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 24, left: 24, right: 24))
..marginButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 24, left: 24, right: 24))
..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black))
..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail))
..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton))
@@ -107,6 +109,7 @@ mixin MessageDialogActionMixin {
..title(title ?? '')
..content(message)
..addIcon(icon)
..paddingButton(paddingButton)
..margin(const EdgeInsets.only(bottom: 42))
..widthDialog(responsiveUtils.getSizeScreenWidth(context))
..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton)
@@ -117,7 +120,7 @@ mixin MessageDialogActionMixin {
)
..marginIcon(EdgeInsets.zero)
..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12))
..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44))
..marginButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44))
..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black))
..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail))
..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton))
@@ -187,6 +190,7 @@ mixin MessageDialogActionMixin {
..title(title ?? '')
..content(message)
..addIcon(icon)
..paddingButton(paddingButton)
..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton)
..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton)
..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null)
@@ -195,7 +199,7 @@ mixin MessageDialogActionMixin {
: const EdgeInsetsDirectional.symmetric(horizontal: 24))
..marginIcon(EdgeInsets.zero)
..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12))
..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44))
..marginButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44))
..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black))
..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail))
..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton))
@@ -92,6 +92,7 @@ import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_st
import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart';
import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart';
import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.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:universal_html/html.dart' as html;
@@ -164,7 +165,6 @@ class ComposerController extends BaseController with DragDropFileMixin implement
FocusNode? ccAddressFocusNodeKeyboard;
FocusNode? bccAddressFocusNodeKeyboard;
StreamSubscription<html.Event>? _subscriptionOnBeforeUnload;
StreamSubscription<html.Event>? _subscriptionOnDragEnter;
StreamSubscription<html.Event>? _subscriptionOnDragOver;
StreamSubscription<html.Event>? _subscriptionOnDragLeave;
@@ -213,7 +213,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
super.onInit();
if (PlatformInfo.isWeb) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_listenBrowserTabRefresh();
_triggerBrowserEventListener();
});
richTextWebController = getBinding<RichTextWebController>();
} else {
@@ -244,7 +244,6 @@ class ComposerController extends BaseController with DragDropFileMixin implement
emailContentsViewState.value = Right(UIClosedState());
identitySelected.value = null;
listFromIdentities.clear();
_subscriptionOnBeforeUnload?.cancel();
_subscriptionOnDragEnter?.cancel();
_subscriptionOnDragOver?.cancel();
_subscriptionOnDragLeave?.cancel();
@@ -353,11 +352,17 @@ class ComposerController extends BaseController with DragDropFileMixin implement
}
@override
void handleExceptionAction({Failure? failure, Exception? exception}) {
super.handleExceptionAction(failure: failure, exception: exception);
if (failure is GetAllIdentitiesFailure) {
_handleGetAllIdentitiesFailure(failure);
void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) {
if (failure is GetAllIdentitiesFailure && exception is! BadCredentialsException) {
_handleGetAllIdentitiesFailure();
}
super.handleUrgentExceptionOnMobile(failure: failure, exception: exception);
}
@override
Future<void> handleBrowserReloadAction(html.Event event) async {
await _removeComposerCacheOnWebInteractor.execute();
await _saveComposerCacheOnWebAction();
}
void _listenStreamEvent() {
@@ -377,13 +382,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
}
}
void _listenBrowserTabRefresh() {
_subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async {
await _removeComposerCacheOnWebInteractor.execute();
await _saveComposerCacheOnWebAction();
});
void _triggerBrowserEventListener() {
_subscriptionOnDragEnter = html.window.onDragEnter.listen((event) {
event.preventDefault();
@@ -1966,9 +1965,8 @@ class ComposerController extends BaseController with DragDropFileMixin implement
);
}
void _handleGetAllIdentitiesFailure(GetAllIdentitiesFailure failure) async {
log('ComposerController::_handleGetAllIdentitiesFailure:failure: $failure');
if (composerArguments.value?.emailActionType == EmailActionType.editSendingEmail && PlatformInfo.isMobile) {
Future<void> _handleGetAllIdentitiesFailure() async {
if (composerArguments.value?.emailActionType == EmailActionType.editSendingEmail) {
final signatureContent = await htmlEditorApi?.getSignatureContent();
log('ComposerController::_handleGetAllIdentitiesFailure:signatureContent: $signatureContent');
if (signatureContent?.isNotEmpty == true) {
@@ -203,17 +203,6 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
String _getTokenAsBearerHeader(String token) => 'Bearer $token';
bool get isAppRunning {
switch(_authenticationType) {
case AuthenticationType.basic:
return _authorization != null;
case AuthenticationType.oidc:
return _configOIDC != null && _token != null;
case AuthenticationType.none:
return false;
}
}
Future<PersonalAccount> _updateCurrentAccount({required TokenOIDC tokenOIDC}) async {
final currentAccount = await _accountCacheManager.getCurrentAccount();
@@ -172,8 +172,8 @@ class LoginController extends ReloadableController {
}
@override
void handleExceptionAction({Failure? failure, Exception? exception}) {
logError('LoginController::handleExceptionAction:exception: $exception | failure: ${failure.runtimeType}');
void handleUrgentException({Failure? failure, Exception? exception}) {
logError('LoginController::handleUrgentException:Exception: $exception | Failure: $failure');
if (failure is CheckOIDCIsAvailableFailure ||
failure is GetStoredOidcConfigurationFailure ||
failure is GetOIDCConfigurationFailure ||
@@ -185,7 +185,7 @@ class LoginController extends ReloadableController {
} else if (failure is GetSessionFailure) {
clearAllData();
} else {
super.handleExceptionAction(failure: failure, exception: exception);
super.handleUrgentException(failure: failure, exception: exception);
}
}
@@ -153,6 +153,7 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:tmail_ui_user/main/routes/route_utils.dart';
import 'package:tmail_ui_user/main/utils/email_receive_manager.dart';
import 'package:tmail_ui_user/main/utils/ios_notification_manager.dart';
import 'package:universal_html/html.dart' as html;
import 'package:uuid/uuid.dart';
class MailboxDashBoardController extends ReloadableController {
@@ -407,14 +408,19 @@ class MailboxDashBoardController extends ReloadableController {
}
@override
void handleExceptionAction({Failure? failure, Exception? exception}) {
super.handleExceptionAction(failure: failure, exception: exception);
if (failure is SendEmailFailure &&
exception is NoNetworkError &&
PlatformInfo.isMobile
) {
log('MailboxDashBoardController::handleExceptionAction(): $failure');
void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) {
if (failure is SendEmailFailure && exception is NoNetworkError) {
_storeSendingEmailInCaseOfSendingFailureInMobile(failure);
} else {
super.handleUrgentExceptionOnMobile(failure: failure, exception: exception);
}
}
@override
Future<void> handleBrowserBeforeReloadAction(html.Event event) async {
if (event is html.BeforeUnloadEvent
&& composerOverlayState.value == ComposerOverlayState.active) {
event.preventDefault();
}
}
@@ -70,7 +70,7 @@ class DeleteIdentityDialogBuilder extends StatelessWidget {
fontSize: 17,
fontWeight: FontWeight.w500,
color: Colors.white))
..paddingButton(const EdgeInsets.only(bottom: 24, left: 24, right: 24))
..marginButton(const EdgeInsets.only(bottom: 24, left: 24, right: 24))
..backgroundColor(Colors.black26)
..onConfirmButtonAction(AppLocalizations.of(context).delete, () =>
onDeleteIdentityAction.call())
@@ -220,11 +220,10 @@ class ThreadController extends BaseController with EmailActionController {
}
@override
void handleExceptionAction({Failure? failure, Exception? exception}) {
super.handleExceptionAction(failure: failure, exception: exception);
logError('ThreadController::handleExceptionAction(): failure: $failure | exception: $exception');
void handleUrgentException({Failure? failure, Exception? exception}) {
_resetLoadingMore();
clearState();
super.handleUrgentException(failure: failure, exception: exception);
}
void _resetLoadingMore() {
+19 -1
View File
@@ -1,5 +1,5 @@
{
"@@last_modified": "2024-07-10T13:53:40.940363",
"@@last_modified": "2024-07-12T12:54:25.065342",
"initializing_data": "Initializing data...",
"@initializing_data": {
"type": "text",
@@ -3933,5 +3933,23 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"reconnect": "Reconnect",
"@reconnect": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"sessionExpired": "Session expired",
"@sessionExpired": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"dialogMessageSessionHasExpired": "The current session has expired. Please reconnect to the server",
"@dialogMessageSessionHasExpired": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
@@ -4113,4 +4113,25 @@ class AppLocalizations {
name: 'requestReadReceiptHasBeenDisabled'
);
}
String get reconnect {
return Intl.message(
'Reconnect',
name: 'reconnect',
);
}
String get sessionExpired {
return Intl.message(
'Session expired',
name: 'sessionExpired',
);
}
String get dialogMessageSessionHasExpired {
return Intl.message(
'The current session has expired. Please reconnect to the server',
name: 'dialogMessageSessionHasExpired',
);
}
}