From c4d0c2760374e5e773cb1c33fd3db6f7bb831cc7 Mon Sep 17 00:00:00 2001 From: DatDang Date: Mon, 23 Sep 2024 13:38:45 +0700 Subject: [PATCH] TF-3157 Implement web socket push --- .../0053-web-socket-data-synchronization.md | 21 ++ lib/features/base/action/ui_action.dart | 6 +- lib/features/base/base_controller.dart | 29 ++- .../mailbox_dashboard_controller.dart | 6 +- .../datasource/web_socket_datasource.dart | 6 + .../remote_web_socket_datasource_impl.dart | 76 ++++++ .../data/model/web_socket_ticket.dart | 33 +++ .../data/network/web_socket_api.dart | 43 ++++ .../web_socket_repository_impl.dart | 14 ++ .../exceptions/web_socket_exceptions.dart | 5 + .../repository/web_socket_repository.dart | 6 + .../domain/state/web_socket_push_state.dart | 19 ++ .../connect_web_socket_interactor.dart | 39 +++ ...ush_notification_state_change_action.dart} | 10 +- .../web_socket_interactor_bindings.dart | 39 +++ .../controller/fcm_base_controller.dart | 25 -- .../controller/fcm_message_controller.dart | 107 ++------ .../controller/fcm_token_controller.dart | 4 +- .../controller/push_base_controller.dart | 109 ++++++++ .../controller/web_socket_controller.dart | 65 +++++ .../listener/email_change_listener.dart | 2 +- .../listener/mailbox_change_listener.dart | 2 +- .../bindings/network/network_bindings.dart | 2 + .../controller/push_base_controller_test.dart | 236 ++++++++++++++++++ 24 files changed, 773 insertions(+), 131 deletions(-) create mode 100644 docs/adr/0053-web-socket-data-synchronization.md create mode 100644 lib/features/push_notification/data/datasource/web_socket_datasource.dart create mode 100644 lib/features/push_notification/data/datasource_impl/remote_web_socket_datasource_impl.dart create mode 100644 lib/features/push_notification/data/model/web_socket_ticket.dart create mode 100644 lib/features/push_notification/data/network/web_socket_api.dart create mode 100644 lib/features/push_notification/data/repository/web_socket_repository_impl.dart create mode 100644 lib/features/push_notification/domain/exceptions/web_socket_exceptions.dart create mode 100644 lib/features/push_notification/domain/repository/web_socket_repository.dart create mode 100644 lib/features/push_notification/domain/state/web_socket_push_state.dart create mode 100644 lib/features/push_notification/domain/usecases/connect_web_socket_interactor.dart rename lib/features/push_notification/presentation/action/{fcm_action.dart => push_notification_state_change_action.dart} (82%) create mode 100644 lib/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart delete mode 100644 lib/features/push_notification/presentation/controller/fcm_base_controller.dart create mode 100644 lib/features/push_notification/presentation/controller/push_base_controller.dart create mode 100644 lib/features/push_notification/presentation/controller/web_socket_controller.dart create mode 100644 test/features/push_notification/presentation/controller/push_base_controller_test.dart diff --git a/docs/adr/0053-web-socket-data-synchronization.md b/docs/adr/0053-web-socket-data-synchronization.md new file mode 100644 index 000000000..0e8bb4b0f --- /dev/null +++ b/docs/adr/0053-web-socket-data-synchronization.md @@ -0,0 +1,21 @@ +# 53. Web socket data synchronization + +Date: 2024-11-10 + +## Status + +Accepted + +## Context + +- Currently Twake Mail web use Firebase Cloud Messaging to sync data on real time +- JMAP already implemented web socket push, which is more optimized for web + +## Decision + +- Web socket is implemented for real time update data for Twake Mail web +- Service worker is implemented for background tasks, helping web socket working in background + +## Consequences + +- Twake Mail web now no longer depends on Firebase Cloud Messaging, using web socket to update users' latest data diff --git a/lib/features/base/action/ui_action.dart b/lib/features/base/action/ui_action.dart index dfe9b368c..4232b24b2 100644 --- a/lib/features/base/action/ui_action.dart +++ b/lib/features/base/action/ui_action.dart @@ -7,11 +7,9 @@ abstract class Action with EquatableMixin {} abstract class UIAction extends Action {} -abstract class FcmAction extends Action {} - -abstract class FcmStateChangeAction extends FcmAction { +abstract class PushNotificationStateChangeAction extends Action { final TypeName typeName; final jmap.State newState; - FcmStateChangeAction(this.typeName, this.newState); + PushNotificationStateChangeAction(this.typeName, this.newState); } \ No newline at end of file diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index ecb070a56..4ec33848a 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -23,8 +23,9 @@ 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/account/authentication_type.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/message_dialog_action_mixin.dart'; @@ -44,14 +45,17 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oi 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'; @@ -372,6 +376,29 @@ abstract class BaseController extends GetxController } } + void injectWebSocket(Session? session, AccountId? accountId) { + try { + 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; diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 23191f91c..edef20674 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -574,7 +574,11 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo injectAutoCompleteBindings(session, currentAccountId); injectRuleFilterBindings(session, currentAccountId); injectVacationBindings(session, currentAccountId); - injectFCMBindings(session, currentAccountId); + if (PlatformInfo.isWeb) { + injectWebSocket(session, currentAccountId); + } else { + injectFCMBindings(session, currentAccountId); + } _getVacationResponse(); spamReportController.getSpamReportStateAction(); diff --git a/lib/features/push_notification/data/datasource/web_socket_datasource.dart b/lib/features/push_notification/data/datasource/web_socket_datasource.dart new file mode 100644 index 000000000..6dbfde421 --- /dev/null +++ b/lib/features/push_notification/data/datasource/web_socket_datasource.dart @@ -0,0 +1,6 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; + +abstract class WebSocketDatasource { + Stream getWebSocketChannel(Session session, AccountId accountId); +} \ No newline at end of file diff --git a/lib/features/push_notification/data/datasource_impl/remote_web_socket_datasource_impl.dart b/lib/features/push_notification/data/datasource_impl/remote_web_socket_datasource_impl.dart new file mode 100644 index 000000000..8d9668796 --- /dev/null +++ b/lib/features/push_notification/data/datasource_impl/remote_web_socket_datasource_impl.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:core/utils/app_logger.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/extensions/session_extension.dart'; +import 'package:rxdart/transformers.dart'; +import 'package:tmail_ui_user/features/push_notification/data/datasource/web_socket_datasource.dart'; +import 'package:tmail_ui_user/features/push_notification/data/network/web_socket_api.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/exceptions/web_socket_exceptions.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class RemoteWebSocketDatasourceImpl implements WebSocketDatasource { + final WebSocketApi _webSocketApi; + final ExceptionThrower _exceptionThrower; + + const RemoteWebSocketDatasourceImpl(this._webSocketApi, this._exceptionThrower); + + @override + Stream getWebSocketChannel(Session session, AccountId accountId) { + return Stream + .castFrom(_getWebSocketChannel(session, accountId)) + .doOnError(_exceptionThrower.throwException); + } + + Stream _getWebSocketChannel( + Session session, + AccountId accountId, + [int retryRemained = 3] + ) async* { + try { + _verifyWebSocketCapabilities(session, accountId); + final webSocketTicket = await _webSocketApi.getWebSocketTicket(session, accountId); + final webSocketUri = _getWebSocketUri(session, accountId); + + final webSocketChannel = WebSocketChannel.connect( + Uri.parse('$webSocketUri?ticket=$webSocketTicket')); + await webSocketChannel.ready; + webSocketChannel.sink.add(jsonEncode({"@type": "WebSocketPushEnable"})); + + yield* webSocketChannel.stream; + } catch (e) { + logError('RemoteWebSocketDatasourceImpl::getWebSocketChannel():error: $e'); + if (retryRemained > 0) { + yield* _getWebSocketChannel(session, accountId, retryRemained - 1); + } else { + rethrow; + } + } + } + + void _verifyWebSocketCapabilities(Session session, AccountId accountId) { + if (!CapabilityIdentifier.jmapWebSocket.isSupported(session, accountId) + || !CapabilityIdentifier.jmapWebSocketTicket.isSupported(session, accountId) + ) { + throw WebSocketPushNotSupportedException(); + } + } + + Uri _getWebSocketUri(Session session, AccountId accountId) { + final webSocketCapability = session.getCapabilityProperties( + accountId, + CapabilityIdentifier.jmapWebSocket); + if (webSocketCapability?.supportsPush != true) { + throw WebSocketPushNotSupportedException(); + } + final webSocketUri = webSocketCapability?.url; + if (webSocketUri == null) throw WebSocketUriUnavailableException(); + + return webSocketUri; + } +} \ No newline at end of file diff --git a/lib/features/push_notification/data/model/web_socket_ticket.dart b/lib/features/push_notification/data/model/web_socket_ticket.dart new file mode 100644 index 000000000..e3cb6dfbf --- /dev/null +++ b/lib/features/push_notification/data/model/web_socket_ticket.dart @@ -0,0 +1,33 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'web_socket_ticket.g.dart'; + +@JsonSerializable(includeIfNull: false) +class WebSocketTicket with EquatableMixin { + final String? value; + final String? clientAddress; + final DateTime? generatedOn; + final DateTime? validUntil; + final String? username; + + WebSocketTicket({ + required this.value, + required this.clientAddress, + required this.generatedOn, + required this.validUntil, + required this.username, + }); + + factory WebSocketTicket.fromJson(Map json) => _$WebSocketTicketFromJson(json); + Map toJson() => _$WebSocketTicketToJson(this); + + @override + List get props => [ + value, + clientAddress, + generatedOn, + validUntil, + username, + ]; +} \ No newline at end of file diff --git a/lib/features/push_notification/data/network/web_socket_api.dart b/lib/features/push_notification/data/network/web_socket_api.dart new file mode 100644 index 000000000..181bf1128 --- /dev/null +++ b/lib/features/push_notification/data/network/web_socket_api.dart @@ -0,0 +1,43 @@ + +import 'package:core/data/network/dio_client.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/web_socket_ticket_capability.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:model/extensions/session_extension.dart'; +import 'package:tmail_ui_user/features/push_notification/data/model/web_socket_ticket.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/exceptions/web_socket_exceptions.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; + +class WebSocketApi { + final DioClient _dioClient; + + WebSocketApi(this._dioClient); + + Future getWebSocketTicket( + Session session, + AccountId accountId + ) async { + requireCapability( + session, + accountId, + [CapabilityIdentifier.jmapWebSocketTicket]); + final webSocketTicketCapability = session.getCapabilityProperties( + accountId, + CapabilityIdentifier.jmapWebSocketTicket); + + final webSocketTicketGenerationUrl = webSocketTicketCapability?.generationEndpoint; + if (webSocketTicketGenerationUrl == null) { + throw WebSocketTicketUnavailableException(); + } + final webSocketTicketGenerationResponse = await _dioClient.post( + '$webSocketTicketGenerationUrl'); + final webSocketTicket = WebSocketTicket.fromJson( + webSocketTicketGenerationResponse); + if (webSocketTicket.value == null) { + throw WebSocketTicketUnavailableException(); + } + + return webSocketTicket.value!; + } +} \ No newline at end of file diff --git a/lib/features/push_notification/data/repository/web_socket_repository_impl.dart b/lib/features/push_notification/data/repository/web_socket_repository_impl.dart new file mode 100644 index 000000000..0f7ecd93c --- /dev/null +++ b/lib/features/push_notification/data/repository/web_socket_repository_impl.dart @@ -0,0 +1,14 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/push_notification/data/datasource/web_socket_datasource.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/web_socket_repository.dart'; + +class WebSocketRepositoryImpl implements WebSocketRepository { + final WebSocketDatasource _webSocketDatasource; + + WebSocketRepositoryImpl(this._webSocketDatasource); + + @override + Stream getWebSocketChannel(Session session, AccountId accountId) + => _webSocketDatasource.getWebSocketChannel(session, accountId); +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/exceptions/web_socket_exceptions.dart b/lib/features/push_notification/domain/exceptions/web_socket_exceptions.dart new file mode 100644 index 000000000..0f0a3c789 --- /dev/null +++ b/lib/features/push_notification/domain/exceptions/web_socket_exceptions.dart @@ -0,0 +1,5 @@ +class WebSocketPushNotSupportedException implements Exception {} + +class WebSocketUriUnavailableException implements Exception {} + +class WebSocketTicketUnavailableException implements Exception {} \ No newline at end of file diff --git a/lib/features/push_notification/domain/repository/web_socket_repository.dart b/lib/features/push_notification/domain/repository/web_socket_repository.dart new file mode 100644 index 000000000..158272d39 --- /dev/null +++ b/lib/features/push_notification/domain/repository/web_socket_repository.dart @@ -0,0 +1,6 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; + +abstract class WebSocketRepository { + Stream getWebSocketChannel(Session session, AccountId accountId); +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/web_socket_push_state.dart b/lib/features/push_notification/domain/state/web_socket_push_state.dart new file mode 100644 index 000000000..ce827ed60 --- /dev/null +++ b/lib/features/push_notification/domain/state/web_socket_push_state.dart @@ -0,0 +1,19 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/push/state_change.dart'; + +class InitializingWebSocketPushChannel extends LoadingState {} + +class WebSocketPushStateReceived extends UIState { + final StateChange stateChange; + + WebSocketPushStateReceived(this.stateChange); + + @override + List get props => [stateChange]; +} + +class WebSocketConnectionFailed extends FeatureFailure { + + WebSocketConnectionFailed({super.exception}); +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/usecases/connect_web_socket_interactor.dart b/lib/features/push_notification/domain/usecases/connect_web_socket_interactor.dart new file mode 100644 index 000000000..f7313152b --- /dev/null +++ b/lib/features/push_notification/domain/usecases/connect_web_socket_interactor.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; + +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/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/push/state_change.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/web_socket_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/web_socket_push_state.dart'; + +class ConnectWebSocketInteractor { + final WebSocketRepository _webSocketRepository; + + ConnectWebSocketInteractor(this._webSocketRepository); + + Stream> execute( + Session session, + AccountId accountId + ) async* { + try { + yield Right(InitializingWebSocketPushChannel()); + yield* _webSocketRepository + .getWebSocketChannel(session, accountId) + .map(_toStateChange); + } catch (e) { + logError('ConnectWebSocketInteractor::execute: $e'); + yield Left(WebSocketConnectionFailed(exception: e)); + } + } + + Either _toStateChange(dynamic data) { + if (data is String) { + data = jsonDecode(data); + } + return Right(WebSocketPushStateReceived(StateChange.fromJson(data))); + } +} \ No newline at end of file diff --git a/lib/features/push_notification/presentation/action/fcm_action.dart b/lib/features/push_notification/presentation/action/push_notification_state_change_action.dart similarity index 82% rename from lib/features/push_notification/presentation/action/fcm_action.dart rename to lib/features/push_notification/presentation/action/push_notification_state_change_action.dart index f04eab4b5..bf84e00c3 100644 --- a/lib/features/push_notification/presentation/action/fcm_action.dart +++ b/lib/features/push_notification/presentation/action/push_notification_state_change_action.dart @@ -6,7 +6,7 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -class SynchronizeEmailOnForegroundAction extends FcmStateChangeAction { +class SynchronizeEmailOnForegroundAction extends PushNotificationStateChangeAction { final AccountId accountId; final Session? session; @@ -22,7 +22,7 @@ class SynchronizeEmailOnForegroundAction extends FcmStateChangeAction { List get props => [typeName, newState, accountId, session]; } -class PushNotificationAction extends FcmStateChangeAction { +class PushNotificationAction extends PushNotificationStateChangeAction { final Session? session; final AccountId accountId; @@ -40,7 +40,7 @@ class PushNotificationAction extends FcmStateChangeAction { List get props => [typeName, newState, accountId, session, userName]; } -class StoreEmailStateToRefreshAction extends FcmStateChangeAction { +class StoreEmailStateToRefreshAction extends PushNotificationStateChangeAction { final AccountId accountId; final UserName userName; @@ -58,7 +58,7 @@ class StoreEmailStateToRefreshAction extends FcmStateChangeAction { List get props => [typeName, newState, accountId, session]; } -class SynchronizeMailboxOnForegroundAction extends FcmStateChangeAction { +class SynchronizeMailboxOnForegroundAction extends PushNotificationStateChangeAction { final AccountId accountId; @@ -72,7 +72,7 @@ class SynchronizeMailboxOnForegroundAction extends FcmStateChangeAction { List get props => [typeName, newState, accountId]; } -class StoreMailboxStateToRefreshAction extends FcmStateChangeAction { +class StoreMailboxStateToRefreshAction extends PushNotificationStateChangeAction { final AccountId accountId; final UserName userName; diff --git a/lib/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart b/lib/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart new file mode 100644 index 000000000..e9787a7ef --- /dev/null +++ b/lib/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart @@ -0,0 +1,39 @@ +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; +import 'package:tmail_ui_user/features/push_notification/data/datasource/web_socket_datasource.dart'; +import 'package:tmail_ui_user/features/push_notification/data/datasource_impl/remote_web_socket_datasource_impl.dart'; +import 'package:tmail_ui_user/features/push_notification/data/network/web_socket_api.dart'; +import 'package:tmail_ui_user/features/push_notification/data/repository/web_socket_repository_impl.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/web_socket_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/connect_web_socket_interactor.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; + +class WebSocketInteractorBindings extends InteractorsBindings { + @override + void bindingsDataSource() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsDataSourceImpl() { + Get.lazyPut(() => RemoteWebSocketDatasourceImpl( + Get.find(), + Get.find(), + )); + } + + @override + void bindingsInteractor() { + Get.lazyPut(() => ConnectWebSocketInteractor(Get.find())); + } + + @override + void bindingsRepository() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => WebSocketRepositoryImpl(Get.find())); + } +} \ No newline at end of file diff --git a/lib/features/push_notification/presentation/controller/fcm_base_controller.dart b/lib/features/push_notification/presentation/controller/fcm_base_controller.dart deleted file mode 100644 index f3c9358a2..000000000 --- a/lib/features/push_notification/presentation/controller/fcm_base_controller.dart +++ /dev/null @@ -1,25 +0,0 @@ - -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'; - -abstract class FcmBaseController { - - void consumeState(Stream> newStateStream) { - newStateStream.listen( - _handleStateStream, - onError: (error, stackTrace) { - logError('FcmBaseController::consumeState():onError:error: $error | stackTrace: $stackTrace'); - } - ); - } - - void _handleStateStream(Either newState) { - newState.fold(handleFailureViewState, handleSuccessViewState); - } - - void handleFailureViewState(Failure failure); - - void handleSuccessViewState(Success success); -} \ No newline at end of file diff --git a/lib/features/push_notification/presentation/controller/fcm_message_controller.dart b/lib/features/push_notification/presentation/controller/fcm_message_controller.dart index 0636a4536..a290e4dbe 100644 --- a/lib/features/push_notification/presentation/controller/fcm_message_controller.dart +++ b/lib/features/push_notification/presentation/controller/fcm_message_controller.dart @@ -1,21 +1,17 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:core/data/network/config/dynamic_url_interceptors.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; -import 'package:fcm/model/type_name.dart'; 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' as jmap; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/push/state_change.dart'; import 'package:model/model.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.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'; @@ -30,10 +26,9 @@ import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_ import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/action/fcm_action.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_base_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/push_base_controller.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/extensions/state_change_extension.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/email_change_listener.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/mailbox_change_listener.dart'; @@ -42,12 +37,7 @@ import 'package:tmail_ui_user/features/push_notification/presentation/utils/fcm_ import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -class FcmMessageController extends FcmBaseController { - - AccountId? _currentAccountId; - Session? _currentSession; - UserName? _userName; - +class FcmMessageController extends PushBaseController { GetAuthenticatedAccountInteractor? _getAuthenticatedAccountInteractor; DynamicUrlInterceptors? _dynamicUrlInterceptors; AuthorizationInterceptors? _authorizationInterceptors; @@ -64,10 +54,9 @@ class FcmMessageController extends FcmBaseController { static FcmMessageController get instance => _instance; + @override void initialize({AccountId? accountId, Session? session}) { - _currentAccountId = accountId; - _currentSession = session; - _userName = session?.username; + super.initialize(accountId: accountId, session: session); _listenTokenStream(); _listenForegroundMessageStream(); @@ -96,15 +85,17 @@ class FcmMessageController extends FcmBaseController { } void _handleForegroundMessageAction(Map payloadData) { - log('FcmMessageController::_handleForegroundMessageAction():payloadData: $payloadData | _currentAccountId: $_currentAccountId'); - if (_currentAccountId != null && _userName != null) { + log('FcmMessageController::_handleForegroundMessageAction():payloadData: $payloadData | accountId: $accountId'); + if (accountId != null && session?.username != null) { final stateChange = FcmUtils.instance.convertFirebaseDataMessageToStateChange(payloadData); - final mapTypeState = stateChange.getMapTypeState(_currentAccountId!); - _mappingTypeStateToAction( + final mapTypeState = stateChange.getMapTypeState(accountId!); + mappingTypeStateToAction( mapTypeState, - _currentAccountId!, - _userName!, - session: _currentSession); + accountId!, + emailChangeListener: EmailChangeListener.instance, + mailboxChangeListener: MailboxChangeListener.instance, + session!.username, + session: session); } } @@ -115,74 +106,6 @@ class FcmMessageController extends FcmBaseController { _getAuthenticatedAccount(stateChange: stateChange); } - void _mappingTypeStateToAction( - Map mapTypeState, - AccountId accountId, - UserName userName, { - bool isForeground = true, - Session? session - }) { - log('FcmMessageController::_mappingTypeStateToAction():mapTypeState: $mapTypeState'); - final listTypeName = mapTypeState.keys - .map((value) => TypeName(value)) - .toList(); - - final listEmailActions = listTypeName - .where((typeName) => typeName == TypeName.emailType || typeName == TypeName.emailDelivery) - .map((typeName) => toFcmAction(typeName, accountId, userName, mapTypeState, isForeground, session: session)) - .whereNotNull() - .toList(); - - log('FcmMessageController::_mappingTypeStateToAction():listEmailActions: $listEmailActions'); - - if (listEmailActions.isNotEmpty) { - EmailChangeListener.instance.dispatchActions(listEmailActions); - } - - final listMailboxActions = listTypeName - .where((typeName) => typeName == TypeName.mailboxType) - .map((typeName) => toFcmAction(typeName, accountId, userName, mapTypeState, isForeground)) - .whereNotNull() - .toList(); - - log('FcmMessageController::_mappingTypeStateToAction():listMailboxActions: $listEmailActions'); - - if (listMailboxActions.isNotEmpty) { - MailboxChangeListener.instance.dispatchActions(listMailboxActions); - } - } - - FcmAction? toFcmAction( - TypeName typeName, - AccountId accountId, - UserName userName, - Map mapTypeState, - isForeground, - { - Session? session - } - ) { - final newState = jmap.State(mapTypeState[typeName.value]); - if (typeName == TypeName.emailType) { - if (isForeground) { - return SynchronizeEmailOnForegroundAction(typeName, newState, accountId, session); - } else { - return StoreEmailStateToRefreshAction(typeName, newState, accountId, userName, session); - } - } else if (typeName == TypeName.emailDelivery) { - if (!isForeground) { - return PushNotificationAction(typeName, newState, session, accountId, userName); - } - } else if (typeName == TypeName.mailboxType) { - if (isForeground) { - return SynchronizeMailboxOnForegroundAction(typeName, newState, accountId); - } else { - return StoreMailboxStateToRefreshAction(typeName, newState, accountId, userName); - } - } - return null; - } - Future _initialAppConfig() async { await Future.wait([ MainBindings().dependencies(), @@ -312,10 +235,12 @@ class FcmMessageController extends FcmBaseController { }) { final mapTypeState = stateChange.getMapTypeState(accountId); - _mappingTypeStateToAction( + mappingTypeStateToAction( mapTypeState, accountId, userName, + emailChangeListener: EmailChangeListener.instance, + mailboxChangeListener: MailboxChangeListener.instance, isForeground: false, session: session); } diff --git a/lib/features/push_notification/presentation/controller/fcm_token_controller.dart b/lib/features/push_notification/presentation/controller/fcm_token_controller.dart index ecb70032a..e85e59860 100644 --- a/lib/features/push_notification/presentation/controller/fcm_token_controller.dart +++ b/lib/features/push_notification/presentation/controller/fcm_token_controller.dart @@ -20,12 +20,12 @@ import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_sto import 'package:tmail_ui_user/features/push_notification/domain/usecases/register_new_firebase_registration_token_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_firebase_registration_interator.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/update_firebase_registration_token_interactor.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_base_controller.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/controller/push_base_controller.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/utils/fcm_utils.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:uuid/uuid.dart'; -class FcmTokenController extends FcmBaseController { +class FcmTokenController extends PushBaseController { FcmTokenController._internal(); diff --git a/lib/features/push_notification/presentation/controller/push_base_controller.dart b/lib/features/push_notification/presentation/controller/push_base_controller.dart new file mode 100644 index 000000000..608011be0 --- /dev/null +++ b/lib/features/push_notification/presentation/controller/push_base_controller.dart @@ -0,0 +1,109 @@ +import 'package:collection/collection.dart'; +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:fcm/model/type_name.dart'; +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' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/base/action/ui_action.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/action/push_notification_state_change_action.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/listener/email_change_listener.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/listener/mailbox_change_listener.dart'; + +abstract class PushBaseController { + Session? session; + AccountId? accountId; + + void consumeState(Stream> newStateStream) { + newStateStream.listen( + _handleStateStream, + onError: (error, stackTrace) { + logError('PushBaseController::consumeState():onError:error: $error | stackTrace: $stackTrace'); + } + ); + } + + void _handleStateStream(Either newState) { + newState.fold(handleFailureViewState, handleSuccessViewState); + } + + void handleFailureViewState(Failure failure); + + void handleSuccessViewState(Success success); + + void initialize({AccountId? accountId, Session? session}) { + this.accountId = accountId; + this.session = session; + } + + void mappingTypeStateToAction( + Map mapTypeState, + AccountId accountId, + UserName userName, { + bool isForeground = true, + Session? session, + required EmailChangeListener emailChangeListener, + required MailboxChangeListener mailboxChangeListener + }) { + log('PushBaseController::mappingTypeStateToAction():mapTypeState: $mapTypeState'); + final listTypeName = mapTypeState.keys + .map((value) => TypeName(value)) + .toList(); + + final listEmailActions = listTypeName + .where((typeName) => typeName == TypeName.emailType || typeName == TypeName.emailDelivery) + .map((typeName) => _toPushNotificationAction(typeName, accountId, userName, mapTypeState, isForeground, session: session)) + .whereNotNull() + .toList(); + + log('PushBaseController::mappingTypeStateToAction():listEmailActions: $listEmailActions'); + + if (listEmailActions.isNotEmpty) { + emailChangeListener.dispatchActions(listEmailActions); + } + + final listMailboxActions = listTypeName + .where((typeName) => typeName == TypeName.mailboxType) + .map((typeName) => _toPushNotificationAction(typeName, accountId, userName, mapTypeState, isForeground)) + .whereNotNull() + .toList(); + + log('PushBaseController::mappingTypeStateToAction():listMailboxActions: $listEmailActions'); + + if (listMailboxActions.isNotEmpty) { + mailboxChangeListener.dispatchActions(listMailboxActions); + } + } + + PushNotificationStateChangeAction? _toPushNotificationAction( + TypeName typeName, + AccountId accountId, + UserName userName, + Map mapTypeState, + isForeground, + {Session? session} + ) { + final newState = jmap.State(mapTypeState[typeName.value]); + if (typeName == TypeName.emailType) { + if (isForeground) { + return SynchronizeEmailOnForegroundAction(typeName, newState, accountId, session); + } else { + return StoreEmailStateToRefreshAction(typeName, newState, accountId, userName, session); + } + } else if (typeName == TypeName.emailDelivery) { + if (!isForeground) { + return PushNotificationAction(typeName, newState, session, accountId, userName); + } + } else if (typeName == TypeName.mailboxType) { + if (isForeground) { + return SynchronizeMailboxOnForegroundAction(typeName, newState, accountId); + } else { + return StoreMailboxStateToRefreshAction(typeName, newState, accountId, userName); + } + } + return null; + } +} \ No newline at end of file diff --git a/lib/features/push_notification/presentation/controller/web_socket_controller.dart b/lib/features/push_notification/presentation/controller/web_socket_controller.dart new file mode 100644 index 000000000..b69cb664e --- /dev/null +++ b/lib/features/push_notification/presentation/controller/web_socket_controller.dart @@ -0,0 +1,65 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/web_socket_push_state.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/connect_web_socket_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/controller/push_base_controller.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/extensions/state_change_extension.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/listener/email_change_listener.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/listener/mailbox_change_listener.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class WebSocketController extends PushBaseController { + WebSocketController._internal(); + + static final WebSocketController _instance = WebSocketController._internal(); + + static WebSocketController get instance => _instance; + + ConnectWebSocketInteractor? _connectWebSocketInteractor; + + @override + void handleFailureViewState(Failure failure) { + logError('WebSocketController::handleFailureViewState():Failure $failure'); + } + + @override + void handleSuccessViewState(Success success) { + log('WebSocketController::handleSuccessViewState():Success $success'); + if (success is WebSocketPushStateReceived) { + _handleWebSocketPushStateReceived(success); + } + } + + @override + void initialize({AccountId? accountId, Session? session}) { + super.initialize(accountId: accountId, session: session); + + _connectWebSocket(accountId, session); + } + + void _connectWebSocket(AccountId? accountId, Session? session) { + _connectWebSocketInteractor = getBinding(); + if (_connectWebSocketInteractor == null || accountId == null || session == null) { + return; + } + + consumeState(_connectWebSocketInteractor!.execute(session, accountId)); + } + + void _handleWebSocketPushStateReceived(WebSocketPushStateReceived success) { + log('WebSocketController::_handleWebSocketPushStateReceived(): $success'); + if (accountId == null || session == null) return; + final stateChange = success.stateChange; + final mapTypeState = stateChange.getMapTypeState(accountId!); + mappingTypeStateToAction( + mapTypeState, + accountId!, + emailChangeListener: EmailChangeListener.instance, + mailboxChangeListener: MailboxChangeListener.instance, + session!.username, + session: session); + } +} \ No newline at end of file diff --git a/lib/features/push_notification/presentation/listener/email_change_listener.dart b/lib/features/push_notification/presentation/listener/email_change_listener.dart index 4839b0a05..5d3ef1175 100644 --- a/lib/features/push_notification/presentation/listener/email_change_listener.dart +++ b/lib/features/push_notification/presentation/listener/email_change_listener.dart @@ -39,7 +39,7 @@ import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_new import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_stored_email_delivery_state_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_email_delivery_state_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_email_state_to_refresh_interactor.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/action/fcm_action.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/action/push_notification_state_change_action.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/change_listener.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; diff --git a/lib/features/push_notification/presentation/listener/mailbox_change_listener.dart b/lib/features/push_notification/presentation/listener/mailbox_change_listener.dart index 1ff676f91..2502f6e0a 100644 --- a/lib/features/push_notification/presentation/listener/mailbox_change_listener.dart +++ b/lib/features/push_notification/presentation/listener/mailbox_change_listener.dart @@ -9,7 +9,7 @@ import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_mailbox_state_to_refresh_interactor.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/action/fcm_action.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/action/push_notification_state_change_action.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/change_listener.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 343a6e829..ce115afe9 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -33,6 +33,7 @@ import 'package:tmail_ui_user/features/manage_account/data/network/rule_filter_a import 'package:tmail_ui_user/features/manage_account/data/network/vacation_api.dart'; import 'package:tmail_ui_user/features/push_notification/data/keychain/keychain_sharing_manager.dart'; import 'package:tmail_ui_user/features/push_notification/data/network/fcm_api.dart'; +import 'package:tmail_ui_user/features/push_notification/data/network/web_socket_api.dart'; import 'package:tmail_ui_user/features/quotas/data/network/quotas_api.dart'; import 'package:tmail_ui_user/features/server_settings/data/network/server_settings_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; @@ -124,6 +125,7 @@ class NetworkBindings extends Bindings { Get.put(FcmApi(Get.find())); Get.put(SpamReportApi(Get.find())); Get.put(ServerSettingsAPI(Get.find())); + Get.put(WebSocketApi(Get.find())); } void _bindingConnection() { diff --git a/test/features/push_notification/presentation/controller/push_base_controller_test.dart b/test/features/push_notification/presentation/controller/push_base_controller_test.dart new file mode 100644 index 000000000..72e414c63 --- /dev/null +++ b/test/features/push_notification/presentation/controller/push_base_controller_test.dart @@ -0,0 +1,236 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:fcm/model/type_name.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/action/push_notification_state_change_action.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/controller/push_base_controller.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/listener/email_change_listener.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/listener/mailbox_change_listener.dart'; + +import 'push_base_controller_test.mocks.dart'; + +class TestPushController extends PushBaseController { + @override + void handleFailureViewState(Failure failure) {} + + @override + void handleSuccessViewState(Success success) {} +} + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + final accountId = AccountId(Id('accountId')); + final userName = UserName('userName'); + + group('push base controller test:', () { + group('mappingTypeStateToAction:', () { + final emailChangeListener = MockEmailChangeListener(); + final mailboxChangeListener = MockMailboxChangeListener(); + + test( + 'should call emailChangeListener.dispatchActions with SynchronizeEmailOnForegroundAction ' + 'when mapTypeState contains emailType ' + 'and isForeground is true', + () { + // arrange + final state = State('some-state'); + final mapTypeState = {TypeName.emailType.value: state.value}; + + // act + final pushBaseController = TestPushController(); + pushBaseController.mappingTypeStateToAction( + mapTypeState, + accountId, + userName, + isForeground: true, + emailChangeListener: emailChangeListener, + mailboxChangeListener: mailboxChangeListener + ); + + // assert + verify( + emailChangeListener.dispatchActions([ + SynchronizeEmailOnForegroundAction( + TypeName.emailType, + state, + accountId, + null, + ), + ]), + ).called(1); + verifyNever(mailboxChangeListener.dispatchActions(any)); + }); + + test( + 'should call emailChangeListener.dispatchActions with StoreEmailStateToRefreshAction ' + 'when mapTypeState contains emailType ' + 'and isForeground is false', + () { + // arrange + final state = State('some-state'); + final mapTypeState = {TypeName.emailType.value: state.value}; + + // act + final pushBaseController = TestPushController(); + pushBaseController.mappingTypeStateToAction( + mapTypeState, + accountId, + userName, + isForeground: false, + emailChangeListener: emailChangeListener, + mailboxChangeListener: mailboxChangeListener + ); + + // assert + verify( + emailChangeListener.dispatchActions([ + StoreEmailStateToRefreshAction( + TypeName.emailType, + state, + accountId, + userName, + null, + ), + ]), + ).called(1); + verifyNever(mailboxChangeListener.dispatchActions(any)); + }); + + test( + 'should call emailChangeListener.dispatchActions with nothing ' + 'when mapTypeState contains emailDelivery ' + 'and isForeground is true', + () { + // arrange + final state = State('some-state'); + final mapTypeState = {TypeName.emailDelivery.value: state.value}; + + // act + final pushBaseController = TestPushController(); + pushBaseController.mappingTypeStateToAction( + mapTypeState, + accountId, + userName, + isForeground: true, + emailChangeListener: emailChangeListener, + mailboxChangeListener: mailboxChangeListener + ); + + // assert + verifyNever(emailChangeListener.dispatchActions(any)); + verifyNever(mailboxChangeListener.dispatchActions(any)); + }); + + test( + 'should call emailChangeListener.dispatchActions with PushNotificationAction ' + 'when mapTypeState contains emailDelivery ' + 'and isForeground is false', + () { + // arrange + final state = State('some-state'); + final mapTypeState = {TypeName.emailDelivery.value: state.value}; + + // act + final pushBaseController = TestPushController(); + pushBaseController.mappingTypeStateToAction( + mapTypeState, + accountId, + userName, + isForeground: false, + emailChangeListener: emailChangeListener, + mailboxChangeListener: mailboxChangeListener + ); + + // assert + verify( + emailChangeListener.dispatchActions([ + PushNotificationAction( + TypeName.emailDelivery, + state, + null, + accountId, + userName, + ), + ]), + ).called(1); + verifyNever(mailboxChangeListener.dispatchActions(any)); + }); + + test( + 'should call mailboxChangeListener.dispatchActions with SynchronizeMailboxOnForegroundAction ' + 'when mapTypeState contains mailboxType ' + 'and isForeground is true', + () { + // arrange + final state = State('some-state'); + final mapTypeState = {TypeName.mailboxType.value: state.value}; + + // act + final pushBaseController = TestPushController(); + pushBaseController.mappingTypeStateToAction( + mapTypeState, + accountId, + userName, + isForeground: true, + emailChangeListener: emailChangeListener, + mailboxChangeListener: mailboxChangeListener + ); + + // assert + verify( + mailboxChangeListener.dispatchActions([ + SynchronizeMailboxOnForegroundAction( + TypeName.mailboxType, + state, + accountId, + ), + ]), + ).called(1); + verifyNever(emailChangeListener.dispatchActions(any)); + }); + + test( + 'should call mailboxChangeListener.dispatchActions with StoreMailboxStateToRefreshAction ' + 'when mapTypeState contains mailboxType ' + 'and isForeground is false', + () { + // arrange + final state = State('some-state'); + final mapTypeState = {TypeName.mailboxType.value: state.value}; + + // act + final pushBaseController = TestPushController(); + pushBaseController.mappingTypeStateToAction( + mapTypeState, + accountId, + userName, + isForeground: false, + emailChangeListener: emailChangeListener, + mailboxChangeListener: mailboxChangeListener + ); + + // assert + verify( + mailboxChangeListener.dispatchActions([ + StoreMailboxStateToRefreshAction( + TypeName.mailboxType, + state, + accountId, + userName, + ), + ]), + ).called(1); + verifyNever(emailChangeListener.dispatchActions(any)); + }); + }); + }); +} \ No newline at end of file