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'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/toast/tmail_toast.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/application_manager.dart'; import 'package:core/utils/fps_manager.dart'; import 'package:core/utils/platform_info.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_dotenv/flutter_dotenv.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/session/session.dart'; import 'package:model/account/authentication_type.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; import 'package:tmail_ui_user/features/base/before_unload_manager.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/login/data/network/interceptors/authorization_interceptors.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/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/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/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/utils/app_utils.dart'; import 'package:universal_html/html.dart' as html; import 'package:uuid/uuid.dart'; abstract class BaseController extends GetxController with MessageDialogActionMixin, PopupContextMenuActionMixin { 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(); bool _isFcmEnabled = false; GetStoredFirebaseRegistrationInteractor? _getStoredFirebaseRegistrationInteractor; DestroyFirebaseRegistrationInteractor? _destroyFirebaseRegistrationInteractor; StreamSubscription? _subscriptionBrowserOnBeforeUnload; StreamSubscription? _subscriptionBrowserOnUnload; final viewState = Rx>(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 handleBrowserBeforeReloadAction(html.Event event) async {} Future handleBrowserReloadAction(html.Event event) async {} @override void onClose() { if (PlatformInfo.isWeb) { _subscriptionBrowserOnBeforeUnload?.cancel(); _subscriptionBrowserOnUnload?.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( (failure) { if (failure is FeatureFailure) { final isUrgentException = _validateUrgentException(failure.exception); if (isUrgentException) { handleUrgentException(failure: failure, exception: failure.exception); } else { handleFailureViewState(failure); } } else { handleFailureViewState(failure); } }, 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) { _executeBeforeUnloadAndLogOut(); } } 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) { _executeBeforeUnloadAndLogOut(); } } Future _executeBeforeUnloadAndLogOut() async { await executeBeforeUnload(); clearDataAndGoToLoginPage(); } 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 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) { final mapEnvData = Map.from(dotenv.env); await AppUtils.loadFcmConfigFileToEnv(currentMapEnvData: mapEnvData); 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'); } } 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 (Get.currentRoute != AppRoutes.login) { pushAndPopAll( AppRoutes.login, arguments: LoginArguments( PlatformInfo.isMobile ? LoginFormType.dnsLookupForm : LoginFormType.none ) ); } } void logout(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(); } } } 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 executeBeforeUnload() async { final beforeUnloadManager = getBinding(); await beforeUnloadManager?.executeBeforeUnloadListeners(); } Future clearDataAndGoToLoginPage() async { log('$runtimeType::clearDataAndGoToLoginPage:'); await clearAllData(); goToLogin(); } Future clearAllData() async { if (isAuthenticatedWithOidc) { await _clearOidcAuthData(); } else { await _clearBasicAuthData(); } } 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(); } }