import 'dart:async'; import 'package:core/core.dart'; import 'package:flutter/services.dart' as services; import 'package:contact/contact/model/capability_contact.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/firebase_capability.dart'; import 'package:fcm/model/firebase_registration_id.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:forward/forward/capability_forward.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'; import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:model/model.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart'; import 'package:tmail_ui_user/features/base/mixin/logout_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/email/presentation/bindings/mdn_interactor_bindings.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/logout_exception.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; 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/mailbox_dashboard/presentation/bindings/contact_autocomplete_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/tmail_autocomplete_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/log_out_oidc_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/bindings/email_rules_interactor_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/forward/bindings/forwarding_interactors_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart'; import 'package:tmail_ui_user/features/push_notification/domain/exceptions/web_socket_exceptions.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/destroy_firebase_registration_state.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/get_stored_firebase_registration_state.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/destroy_firebase_registration_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_stored_firebase_registration_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/config/fcm_configuration.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_message_controller.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_token_controller.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/controller/web_socket_controller.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_receiver.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.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/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/universal_import/html_stub.dart' as html; import 'package:tmail_ui_user/main/utils/toast_manager.dart'; import 'package:tmail_ui_user/main/utils/twake_app_manager.dart'; import 'package:uuid/uuid.dart'; abstract class BaseController extends GetxController with MessageDialogActionMixin, PopupContextMenuActionMixin, LogoutMixin { final CachingManager cachingManager = Get.find(); final LanguageCacheManager languageCacheManager = Get.find(); final AuthorizationInterceptors authorizationInterceptors = Get.find(); final AuthorizationInterceptors authorizationIsolateInterceptors = Get.find(tag: BindingTag.isolateTag); final DynamicUrlInterceptors dynamicUrlInterceptors = Get.find(); final DeleteCredentialInteractor deleteCredentialInteractor = Get.find(); final LogoutOidcInteractor logoutOidcInteractor = Get.find(); final DeleteAuthorityOidcInteractor deleteAuthorityOidcInteractor = Get.find(); final AppToast appToast = Get.find(); final ImagePaths imagePaths = Get.find(); final ResponsiveUtils responsiveUtils = Get.find(); final Uuid uuid = Get.find(); final ApplicationManager applicationManager = Get.find(); final ToastManager toastManager = Get.find(); final TwakeAppManager twakeAppManager = Get.find(); bool _isFcmEnabled = false; GetStoredFirebaseRegistrationInteractor? _getStoredFirebaseRegistrationInteractor; DestroyFirebaseRegistrationInteractor? _destroyFirebaseRegistrationInteractor; StreamSubscription? _onBeforeUnloadBrowserSubscription; StreamSubscription? _onUnloadBrowserSubscription; final viewState = Rx>(Right(UIState.idle)); FpsCallback? fpsCallback; @override void onReady() { super.onReady(); if (PlatformInfo.isWeb) { _triggerBrowserReloadListener(); } } void _triggerBrowserReloadListener() { _onBeforeUnloadBrowserSubscription = html.window.onBeforeUnload.listen(onBeforeUnloadBrowserListener); _onUnloadBrowserSubscription = html.window.onUnload.listen(onUnloadBrowserListener); } Future onBeforeUnloadBrowserListener(html.Event event) async {} Future onUnloadBrowserListener(html.Event event) async {} @override void onClose() { if (PlatformInfo.isWeb) { _onBeforeUnloadBrowserSubscription?.cancel(); _onUnloadBrowserSubscription?.cancel(); } super.onClose(); } void consumeState(Stream> newStateStream) async { newStateStream.listen(onData, onError: onError, onDone: onDone); } void dispatchState(Either newState) { viewState.value = newState; } void clearState() { viewState.value = Right(UIState.idle); } void onData(Either newState) { viewState.value = newState; viewState.value.fold(onDataFailureViewState, handleSuccessViewState); } 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); } } void onDone() {} 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) { _handleConnectionErrorException(); } else if (exception is BadCredentialsException) { _handleBadCredentialsException(); } } void handleUrgentExceptionOnWeb({Failure? failure, Exception? exception}) { logError('$runtimeType::handleUrgentExceptionOnWeb():Failure: $failure | Exception: $exception'); if (exception is NoNetworkError) { _handleNotNetworkErrorException(); } else if (exception is ConnectionError) { _handleConnectionErrorException(); } else if (exception is BadCredentialsException) { _handleBadCredentialsException(); } } Future _executeBeforeReconnectAndLogOut() async { await executeBeforeReconnect(); clearDataAndGoToLoginPage(); } void onCancelReconnectWhenSessionExpired() {} void _handleConnectionErrorException() { if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage( currentOverlayContext!, AppLocalizations.of(currentContext!).connectionError); } } void _handleNotNetworkErrorException() { if (currentOverlayContext != null && currentContext != null) { appToast.showToastMessage( currentOverlayContext!, AppLocalizations.of(currentContext!).no_internet_connection, actionName: AppLocalizations.of(currentContext!).skip, onActionClick: ToastView.dismiss, leadingSVGIcon: imagePaths.icNotConnection, backgroundColor: AppColor.textFieldErrorBorderColor, textColor: Colors.white, infinityToast: true); } } void _handleBadCredentialsException() { log('$runtimeType::_handleBadCredentialsException:'); if (!twakeAppManager.isComposerOpened) { _performReconnection(); return; } if (currentContext == null) { _performSaveAndReconnection(); return; } final appLocalizations = AppLocalizations.of(currentContext!); showConfirmDialogAction( currentContext!, appLocalizations.messageWarningDialogWhenExpiredOIDCTokenAndReconnection, appLocalizations.saveAndRefresh, title: appLocalizations.sessionExpired, cancelTitle: appLocalizations.no, alignCenter: true, outsideDismissible: false, titleActionButtonMaxLines: 1, marginIcon: EdgeInsetsDirectional.zero, icon: SvgPicture.asset(imagePaths.icTMailLogo, width: 64, height: 64), onConfirmAction: _performSaveAndReconnection, onCancelAction: _performReconnection, onCloseButtonAction: _performCloseReconnectionConfirmDialog ); } void _performSaveAndReconnection() { if (PlatformInfo.isWeb) { _executeBeforeReconnectAndLogOut(); } else if (PlatformInfo.isMobile) { clearDataAndGoToLoginPage(); } } void _performReconnection() { clearDataAndGoToLoginPage(); } void _performCloseReconnectionConfirmDialog() { popBack(); } void onDataFailureViewState(Failure failure) { if (failure is FeatureFailure) { final isUrgentException = validateUrgentException(failure.exception); if (isUrgentException) { handleUrgentException(failure: failure, exception: failure.exception); } else { handleFailureViewState(failure); } } else { handleFailureViewState(failure); } } void handleFailureViewState(Failure failure) async { logError('$runtimeType::handleFailureViewState():Failure = $failure'); if (failure is LogoutOidcFailure) { if (_isFcmEnabled) { _getStoredFirebaseRegistrationFromCache(); } else { await clearDataAndGoToLoginPage(); } } else if (failure is GetStoredFirebaseRegistrationFailure || failure is DestroyFirebaseRegistrationFailure) { await clearDataAndGoToLoginPage(); } } void handleSuccessViewState(Success success) async { log('$runtimeType::handleSuccessViewState():Success = ${success.runtimeType}'); if (success is LogoutOidcSuccess) { if (_isFcmEnabled) { _getStoredFirebaseRegistrationFromCache(); } else { await clearDataAndGoToLoginPage(); } } else if (success is GetStoredFirebaseRegistrationSuccess) { _destroyFirebaseRegistration(success.firebaseRegistration.id!); } else if (success is DestroyFirebaseRegistrationSuccess) { await clearDataAndGoToLoginPage(); } } void startFpsMeter() { FpsManager().start(); fpsCallback = (fpsInfo) { log('$runtimeType::startFpsMeter(): $fpsInfo'); }; if (fpsCallback != null) { FpsManager().addFpsCallback(fpsCallback!); } } void stopFpsMeter() { FpsManager().stop(); if (fpsCallback != null) { FpsManager().removeFpsCallback(fpsCallback!); } } void injectAutoCompleteBindings(Session? session, AccountId? accountId) { try { ContactAutoCompleteBindings().dependencies(); requireCapability(session!, accountId!, [tmailContactCapabilityIdentifier]); TMailAutoCompleteBindings().dependencies(); } catch (e) { logError('$runtimeType::injectAutoCompleteBindings(): exception: $e'); } } void injectMdnBindings(Session? session, AccountId? accountId) { try { requireCapability(session!, accountId!, [CapabilityIdentifier.jmapMdn]); MdnInteractorBindings().dependencies(); } catch(e) { logError('$runtimeType::injectMdnBindings(): exception: $e'); } } void injectForwardBindings(Session? session, AccountId? accountId) { try { requireCapability(session!, accountId!, [capabilityForward]); ForwardingInteractorsBindings().dependencies(); } catch(e) { logError('$runtimeType::injectForwardBindings(): exception: $e'); } } void injectRuleFilterBindings(Session? session, AccountId? accountId) { try { requireCapability(session!, accountId!, [capabilityRuleFilter]); EmailRulesInteractorBindings().dependencies(); } catch(e) { logError('$runtimeType::injectRuleFilterBindings(): exception: $e'); } } Future injectFCMBindings(Session? session, AccountId? accountId) async { try { requireCapability(session!, accountId!, [FirebaseCapability.fcmIdentifier]); log('$runtimeType::injectFCMBindings: fcmAvailable = ${AppConfig.fcmAvailable}'); if (AppConfig.fcmAvailable) { await FcmConfiguration.initialize(); FcmInteractorBindings().dependencies(); FcmService.instance.initialStreamController(); FcmMessageController.instance.initialize(accountId: accountId, session: session); FcmTokenController.instance.initialBindingInteractor(); await FcmReceiver.instance.onInitialFcmListener(); if (PlatformInfo.isMobile) { await LocalNotificationManager.instance.setUp(groupId: session.username.value); } } else { throw NotSupportFCMException(); } } catch(e) { logError('$runtimeType::injectFCMBindings(): exception: $e'); } } void injectWebSocket(Session? session, AccountId? accountId) { try { log('$runtimeType::injectWebSocket:'); requireCapability( session!, accountId!, [ CapabilityIdentifier.jmapWebSocket, CapabilityIdentifier.jmapWebSocketTicket ] ); final wsCapability = session.getCapabilityProperties( accountId, CapabilityIdentifier.jmapWebSocket); if (wsCapability?.supportsPush != true) { throw WebSocketPushNotSupportedException(); } WebSocketInteractorBindings().dependencies(); WebSocketController.instance.initialize(accountId: accountId, session: session); } catch(e) { logError('$runtimeType::injectWebSocket(): exception: $e'); } } AuthenticationType get authenticationType => authorizationInterceptors.authenticationType; bool get isAuthenticatedWithOidc => authenticationType == AuthenticationType.oidc; bool _isFcmActivated(Session session, AccountId accountId) => FirebaseCapability.fcmIdentifier.isSupported(session, accountId) && AppConfig.fcmAvailable; void goToLogin() { if (PlatformInfo.isMobile) { navigateToTwakeWelcomePage(); } else { navigateToLoginPage(); } } void removeAllPageAndGoToLogin() { if (PlatformInfo.isMobile) { pushAndPopAll(AppRoutes.twakeWelcome); } else { navigateToLoginPage(); } } void navigateToTwakeWelcomePage() { popAndPush(AppRoutes.twakeWelcome); } void navigateToLoginPage() { if (Get.currentRoute == AppRoutes.login) { return; } pushAndPopAll( AppRoutes.login, arguments: LoginArguments(LoginFormType.none)); } void logout( BuildContext context, Session? session, AccountId? accountId, ) { if (PlatformInfo.isMobile) { showLogoutConfirmDialog( context: context, username: session?.username.value ?? '', onConfirmAction: () => _handleLogoutAction(session, accountId), ); } else { _handleLogoutAction(session, accountId); } } Future _handleLogoutAction(Session? session, AccountId? accountId) async { if (session == null || accountId == null) { await clearDataAndGoToLoginPage(); return; } _isFcmEnabled = _isFcmActivated(session, accountId); if (isAuthenticatedWithOidc) { consumeState(logoutOidcInteractor.execute()); } else { if (_isFcmEnabled) { _getStoredFirebaseRegistrationFromCache(); } else { await clearDataAndGoToLoginPage(); } } } Future logoutToSignInNewAccount({ required Session session, required AccountId accountId, required Function onSuccessCallback, required Function({Object? exception}) onFailureCallback, }) async { try { _isFcmEnabled = _isFcmActivated(session, accountId); if (isAuthenticatedWithOidc) { final logoutViewState = await logoutOidcInteractor.execute().last; logoutViewState.foldSuccess( onSuccess: (success) async { await _handleDeleteFCMAndClearData(); onSuccessCallback(); }, onFailure: (failure) async { if (failure is LogoutOidcFailure && _validateUserCancelledLogoutOidcFlow(failure.exception)) { await _handleDeleteFCMAndClearData(); onFailureCallback(exception: UserCancelledLogoutOIDCFlowException()); } else { onFailureCallback(); } }, ); } else { await _handleDeleteFCMAndClearData(); onSuccessCallback(); } } catch (e) { logError('BaseController::logoutToSignInNewAccount:Exception = $e'); onFailureCallback(); } } bool _validateUserCancelledLogoutOidcFlow(dynamic exception) { return exception is services.PlatformException && exception.code == OIDCConstant.endSessionFailedCode; } Future _handleDeleteFCMAndClearData() async { await Future.wait([ if (_isFcmEnabled) _handleDeleteFCMRegistration(), clearAllData(), ]); } Future _handleDeleteFCMRegistration() async { try { _getStoredFirebaseRegistrationInteractor = getBinding(); final fcmRegistration = await _getStoredFirebaseRegistrationInteractor?.execute().last; fcmRegistration?.foldSuccess( onSuccess: (success) async { _destroyFirebaseRegistrationInteractor = getBinding(); await _destroyFirebaseRegistrationInteractor?.execute(success.firebaseRegistration.id!).last; }, onFailure: (failure) {}, ); } catch (e) { logError('BaseController::_handleDeleteFCMRegistration:Exception = $e'); } } void _destroyFirebaseRegistration(FirebaseRegistrationId firebaseRegistrationId) async { _destroyFirebaseRegistrationInteractor = getBinding(); if (_destroyFirebaseRegistrationInteractor != null) { consumeState(_destroyFirebaseRegistrationInteractor!.execute(firebaseRegistrationId)); } else { await clearDataAndGoToLoginPage(); } } void _getStoredFirebaseRegistrationFromCache() async { _getStoredFirebaseRegistrationInteractor = getBinding(); if (_getStoredFirebaseRegistrationInteractor != null) { consumeState(_getStoredFirebaseRegistrationInteractor!.execute()); } else { await clearDataAndGoToLoginPage(); } } Future executeBeforeReconnect() async { final beforeReconnectManager = getBinding(); await beforeReconnectManager?.executeBeforeReconnectListeners(); } Future clearDataAndGoToLoginPage() async { log('$runtimeType::clearDataAndGoToLoginPage:'); await clearAllData(); removeAllPageAndGoToLogin(); } Future clearAllData() async { try { if (isAuthenticatedWithOidc) { await _clearOidcAuthData(); } else { await _clearBasicAuthData(); } } catch (e) { logError('BaseController::clearAllData:Exception = $e'); } } Future _clearBasicAuthData() async { await Future.wait([ deleteCredentialInteractor.execute(), cachingManager.clearAll(), languageCacheManager.removeLanguage(), ]); if (PlatformInfo.isMobile) { await cachingManager.clearAllFileInStorage(); } authorizationInterceptors.clear(); authorizationIsolateInterceptors.clear(); await cachingManager.closeHive(); } Future _clearOidcAuthData() async { await Future.wait([ deleteAuthorityOidcInteractor.execute(), cachingManager.clearAll(), languageCacheManager.removeLanguage(), ]); if (PlatformInfo.isMobile) { await cachingManager.clearAllFileInStorage(); } authorizationIsolateInterceptors.clear(); authorizationInterceptors.clear(); await cachingManager.closeHive(); } int getMinInputLengthAutocomplete({ required Session session, required AccountId accountId, }) { final minInputLength = session.getMinInputLengthAutocomplete(accountId); return minInputLength?.value.toInt() ?? AppConfig.defaultMinInputLengthAutocomplete; } }