From fdbbf855b66b2e4c37fefbec0760c4fa5c2a16b4 Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 16 Aug 2024 10:46:27 +0700 Subject: [PATCH] TF-3082 Warn user and save identity cache on reload/session expired --- .../identity_creator_data_source.dart | 10 ++ ...cal_identity_creator_data_source_impl.dart | 42 ++++++ .../data/model/identity_cache_model.dart | 35 +++++ .../identity_creator_repository_impl.dart | 21 +++ .../domain/model/identity_cache.dart | 24 +++ .../identity_creator_repository.dart | 10 ++ .../save_identity_cache_on_web_state.dart | 10 ++ ...save_identity_cache_on_web_interactor.dart | 33 +++++ .../identity_creator_bindings.dart | 49 ++++++- .../identity_creator_controller.dart | 104 ++++++++++--- .../model/identity_creator_arguments.dart | 7 + ...ic_assets_in_identity_arguments_model.dart | 28 ++++ .../presentation/public_asset_controller.dart | 8 + ...dentity_creator_data_source_impl_test.dart | 69 +++++++++ .../identity_creator_controller_test.dart | 137 ++++++++++++++++-- 15 files changed, 552 insertions(+), 35 deletions(-) create mode 100644 lib/features/identity_creator/data/datasource/identity_creator_data_source.dart create mode 100644 lib/features/identity_creator/data/datasource_impl/local_identity_creator_data_source_impl.dart create mode 100644 lib/features/identity_creator/data/model/identity_cache_model.dart create mode 100644 lib/features/identity_creator/data/repository/identity_creator_repository_impl.dart create mode 100644 lib/features/identity_creator/domain/model/identity_cache.dart create mode 100644 lib/features/identity_creator/domain/repository/identity_creator_repository.dart create mode 100644 lib/features/identity_creator/domain/state/save_identity_cache_on_web_state.dart create mode 100644 lib/features/identity_creator/domain/usecase/save_identity_cache_on_web_interactor.dart create mode 100644 lib/features/public_asset/data/model/public_assets_in_identity_arguments_model.dart create mode 100644 test/features/identity_creator/data/datasource_impl/local_identity_creator_data_source_impl_test.dart diff --git a/lib/features/identity_creator/data/datasource/identity_creator_data_source.dart b/lib/features/identity_creator/data/datasource/identity_creator_data_source.dart new file mode 100644 index 000000000..f9fc955c2 --- /dev/null +++ b/lib/features/identity_creator/data/datasource/identity_creator_data_source.dart @@ -0,0 +1,10 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/model/identity_cache.dart'; + +abstract class IdentityCreatorDataSource { + Future saveIdentityCacheOnWeb( + AccountId accountId, + UserName userName, + {required IdentityCache identityCache}); +} \ No newline at end of file diff --git a/lib/features/identity_creator/data/datasource_impl/local_identity_creator_data_source_impl.dart b/lib/features/identity_creator/data/datasource_impl/local_identity_creator_data_source_impl.dart new file mode 100644 index 000000000..e6d343c6c --- /dev/null +++ b/lib/features/identity_creator/data/datasource_impl/local_identity_creator_data_source_impl.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; +import 'package:tmail_ui_user/features/identity_creator/data/datasource/identity_creator_data_source.dart'; +import 'package:tmail_ui_user/features/identity_creator/data/model/identity_cache_model.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/model/identity_cache.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; +import 'package:universal_html/html.dart'; + +class LocalIdentityCreatorDataSourceImpl implements IdentityCreatorDataSource { + LocalIdentityCreatorDataSourceImpl(this._exceptionThrower); + + final ExceptionThrower _exceptionThrower; + + static const sessionStorageKeyword = 'identityCreatorSessionStorage'; + + @override + Future saveIdentityCacheOnWeb( + AccountId accountId, + UserName userName, + {required IdentityCache identityCache} + ) async { + return Future.sync(() { + final cacheKey = _generateTupleKey(accountId, userName); + Map entries = { + cacheKey: jsonEncode(IdentityCacheModel.fromDomain(identityCache).toJson()) + }; + window.sessionStorage.addAll(entries); + }).catchError(_exceptionThrower.throwException); + } + + String _generateTupleKey(AccountId accountId, UserName userName) { + return TupleKey( + LocalIdentityCreatorDataSourceImpl.sessionStorageKeyword, + accountId.asString, + userName.value + ).toString(); + } +} \ No newline at end of file diff --git a/lib/features/identity_creator/data/model/identity_cache_model.dart b/lib/features/identity_creator/data/model/identity_cache_model.dart new file mode 100644 index 000000000..894a183f2 --- /dev/null +++ b/lib/features/identity_creator/data/model/identity_cache_model.dart @@ -0,0 +1,35 @@ +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/model/identity_cache.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; +import 'package:tmail_ui_user/features/public_asset/data/model/public_assets_in_identity_arguments_model.dart'; + +part 'identity_cache_model.g.dart'; + +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class IdentityCacheModel extends IdentityCache { + IdentityCacheModel({ + required super.identity, + required super.identityActionType, + required super.isDefault, + required this.publicAssetsInIdentityArgumentsModel}) + : super(publicAssetsInIdentityArguments: publicAssetsInIdentityArgumentsModel); + + final PublicAssetsInIdentityArgumentsModel? publicAssetsInIdentityArgumentsModel; + + factory IdentityCacheModel.fromJson(Map json) + => _$IdentityCacheModelFromJson(json); + + Map toJson() => _$IdentityCacheModelToJson(this); + + factory IdentityCacheModel.fromDomain(IdentityCache identityCache) { + return IdentityCacheModel( + identity: identityCache.identity, + identityActionType: identityCache.identityActionType, + isDefault: identityCache.isDefault, + publicAssetsInIdentityArgumentsModel: identityCache.publicAssetsInIdentityArguments == null + ? null + : PublicAssetsInIdentityArgumentsModel.fromDomain(identityCache.publicAssetsInIdentityArguments!) + ); + } +} \ No newline at end of file diff --git a/lib/features/identity_creator/data/repository/identity_creator_repository_impl.dart b/lib/features/identity_creator/data/repository/identity_creator_repository_impl.dart new file mode 100644 index 000000000..f75620929 --- /dev/null +++ b/lib/features/identity_creator/data/repository/identity_creator_repository_impl.dart @@ -0,0 +1,21 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/identity_creator/data/datasource/identity_creator_data_source.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/model/identity_cache.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/repository/identity_creator_repository.dart'; + +class IdentityCreatorRepositoryImpl implements IdentityCreatorRepository { + IdentityCreatorRepositoryImpl(this._identityCreatorDataSource); + + final IdentityCreatorDataSource _identityCreatorDataSource; + + @override + Future saveIdentityCacheOnWeb( + AccountId accountId, + UserName userName, + {required IdentityCache identityCache} + ) => _identityCreatorDataSource.saveIdentityCacheOnWeb( + accountId, + userName, + identityCache: identityCache); +} \ No newline at end of file diff --git a/lib/features/identity_creator/domain/model/identity_cache.dart b/lib/features/identity_creator/domain/model/identity_cache.dart new file mode 100644 index 000000000..a3973d869 --- /dev/null +++ b/lib/features/identity_creator/domain/model/identity_cache.dart @@ -0,0 +1,24 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; +import 'package:tmail_ui_user/features/public_asset/domain/model/public_assets_in_identity_arguments.dart'; + +class IdentityCache with EquatableMixin { + final Identity? identity; + final IdentityActionType identityActionType; + final bool isDefault; + final PublicAssetsInIdentityArguments? publicAssetsInIdentityArguments; + + IdentityCache({ + required this.identity, + required this.identityActionType, + required this.isDefault, + required this.publicAssetsInIdentityArguments}); + + @override + List get props => [ + identity, + identityActionType, + isDefault, + publicAssetsInIdentityArguments]; +} \ No newline at end of file diff --git a/lib/features/identity_creator/domain/repository/identity_creator_repository.dart b/lib/features/identity_creator/domain/repository/identity_creator_repository.dart new file mode 100644 index 000000000..06c606d12 --- /dev/null +++ b/lib/features/identity_creator/domain/repository/identity_creator_repository.dart @@ -0,0 +1,10 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/model/identity_cache.dart'; + +abstract class IdentityCreatorRepository { + Future saveIdentityCacheOnWeb( + AccountId accountId, + UserName userName, + {required IdentityCache identityCache}); +} \ No newline at end of file diff --git a/lib/features/identity_creator/domain/state/save_identity_cache_on_web_state.dart b/lib/features/identity_creator/domain/state/save_identity_cache_on_web_state.dart new file mode 100644 index 000000000..d9a8fde68 --- /dev/null +++ b/lib/features/identity_creator/domain/state/save_identity_cache_on_web_state.dart @@ -0,0 +1,10 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class SavingIdentityCacheOnWeb extends LoadingState {} + +class SaveIdentityCacheOnWebSuccess extends UIState {} + +class SaveIdentityCacheOnWebFailure extends FeatureFailure { + SaveIdentityCacheOnWebFailure({super.exception}); +} \ No newline at end of file diff --git a/lib/features/identity_creator/domain/usecase/save_identity_cache_on_web_interactor.dart b/lib/features/identity_creator/domain/usecase/save_identity_cache_on_web_interactor.dart new file mode 100644 index 000000000..beb87ab07 --- /dev/null +++ b/lib/features/identity_creator/domain/usecase/save_identity_cache_on_web_interactor.dart @@ -0,0 +1,33 @@ +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/user_name.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/model/identity_cache.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/repository/identity_creator_repository.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/state/save_identity_cache_on_web_state.dart'; + +class SaveIdentityCacheOnWebInteractor { + SaveIdentityCacheOnWebInteractor(this._identityCreatorRepository); + + final IdentityCreatorRepository _identityCreatorRepository; + + Stream> execute( + AccountId accountId, + UserName userName, + {required IdentityCache identityCache} + ) async* { + try { + yield Right(SavingIdentityCacheOnWeb()); + await _identityCreatorRepository.saveIdentityCacheOnWeb( + accountId, + userName, + identityCache: identityCache); + yield Right(SaveIdentityCacheOnWebSuccess()); + } catch (exception) { + logError("$runtimeType::execute: $exception"); + yield Left(SaveIdentityCacheOnWebFailure(exception: exception)); + } + } +} \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/identity_creator_bindings.dart b/lib/features/identity_creator/presentation/identity_creator_bindings.dart index 390d3a85a..b2f6ddadb 100644 --- a/lib/features/identity_creator/presentation/identity_creator_bindings.dart +++ b/lib/features/identity_creator/presentation/identity_creator_bindings.dart @@ -1,21 +1,58 @@ import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/identity_creator/data/datasource/identity_creator_data_source.dart'; +import 'package:tmail_ui_user/features/identity_creator/data/datasource_impl/local_identity_creator_data_source_impl.dart'; +import 'package:tmail_ui_user/features/identity_creator/data/repository/identity_creator_repository_impl.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/repository/identity_creator_repository.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/usecase/save_identity_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/utils/identity_utils.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; -class IdentityCreatorBindings extends Bindings { - +class IdentityCreatorBindings extends BaseBindings { @override - void dependencies() { - IdentityInteractorsBindings().dependencies(); - Get.lazyPut(() => VerifyNameInteractor()); - + void bindingsController() { Get.lazyPut(() => IdentityCreatorController( Get.find(), Get.find(), + Get.find(), Get.find() )); } + + @override + void bindingsDataSource() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsDataSourceImpl() { + Get.lazyPut(() => LocalIdentityCreatorDataSourceImpl( + Get.find() + )); + } + + @override + void bindingsInteractor() { + IdentityInteractorsBindings().dependencies(); + Get.lazyPut(() => VerifyNameInteractor()); + Get.lazyPut(() => SaveIdentityCacheOnWebInteractor( + Get.find() + )); + } + + @override + void bindingsRepository() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => IdentityCreatorRepositoryImpl( + Get.find() + )); + } } \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/identity_creator_controller.dart b/lib/features/identity_creator/presentation/identity_creator_controller.dart index 709dd4663..d90fbc76c 100644 --- a/lib/features/identity_creator/presentation/identity_creator_controller.dart +++ b/lib/features/identity_creator/presentation/identity_creator_controller.dart @@ -35,8 +35,12 @@ import 'package:path_provider/path_provider.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:rich_text_composer/views/commons/constants.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/base/before_reconnect_handler.dart'; +import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/model/identity_cache.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/usecase/save_identity_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/model/identity_creator_arguments.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/utils/identity_creator_constants.dart'; @@ -67,10 +71,11 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html hide File; -class IdentityCreatorController extends BaseController with DragDropFileMixin { +class IdentityCreatorController extends BaseController with DragDropFileMixin implements BeforeReconnectHandler { final VerifyNameInteractor _verifyNameInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; + final SaveIdentityCacheOnWebInteractor _saveIdentityCacheOnWebInteractor; final IdentityUtils _identityUtils; final noneEmailAddress = EmailAddress(null, 'None'); @@ -99,6 +104,7 @@ class IdentityCreatorController extends BaseController with DragDropFileMixin { StreamSubscription? _subscriptionOnDragOver; StreamSubscription? _subscriptionOnDragLeave; StreamSubscription? _subscriptionOnDrop; + final _beforeReconnectManager = Get.find(); String? _nameIdentity; String? _contentHtmlEditor; @@ -139,6 +145,7 @@ class IdentityCreatorController extends BaseController with DragDropFileMixin { IdentityCreatorController( this._verifyNameInteractor, this._getAllIdentitiesInteractor, + this._saveIdentityCacheOnWebInteractor, this._identityUtils ); @@ -152,6 +159,7 @@ class IdentityCreatorController extends BaseController with DragDropFileMixin { } log('IdentityCreatorController::onInit():arguments: ${Get.arguments}'); arguments = Get.arguments; + _beforeReconnectManager.addListener(onBeforeReconnect); } @override @@ -243,6 +251,7 @@ class IdentityCreatorController extends BaseController with DragDropFileMixin { _subscriptionOnDragOver?.cancel(); _subscriptionOnDragLeave?.cancel(); _subscriptionOnDrop?.cancel(); + _beforeReconnectManager.removeListener(onBeforeReconnect); super.onClose(); } @@ -277,7 +286,21 @@ class IdentityCreatorController extends BaseController with DragDropFileMixin { if (PlatformInfo.isWeb) { richTextWebController?.editorController.setText(arguments?.identity?.signatureAsString ?? ''); } - publicAssetController?.getOldPublicAssetFromHtmlContent(arguments!.identity!.signatureAsString); + } + _initPublicAssetController(); + } + + void _initPublicAssetController() { + final publicAssetsInIdentityArguments = arguments?.publicAssetsInIdentityArguments; + final htmlSignature = arguments?.identity?.signatureAsString; + + if (publicAssetsInIdentityArguments != null) { + publicAssetController?.restorePreExistingPublicAssetsFromCache( + publicAssetsInIdentityArguments.preExistingPublicAssetIds); + publicAssetController?.restoreNewlyPickedPublicAssetsFromCache( + publicAssetsInIdentityArguments.newlyPickedPublicAssetIds); + } else if (htmlSignature != null && htmlSignature.isNotEmpty) { + publicAssetController?.getOldPublicAssetFromHtmlContent(htmlSignature); } } @@ -378,6 +401,8 @@ class IdentityCreatorController extends BaseController with DragDropFileMixin { listDefaultIdentityIds?.contains(identity?.id) == true ) { isDefaultIdentity.value = true; + } else if (arguments?.isDefault == true) { + isDefaultIdentity.value = true; } else { isDefaultIdentity.value = false; } @@ -438,6 +463,30 @@ class IdentityCreatorController extends BaseController with DragDropFileMixin { return; } + final identityAndPublicAssetArguments = await _generateIdentityAndPublicAssetArguments(); + + if (actionType.value == IdentityActionType.create) { + final generateCreateId = Id(uuid.v1()); + final identityRequest = CreateNewIdentityRequest( + generateCreateId, + identityAndPublicAssetArguments.identity, + publicAssetsInIdentityArguments: identityAndPublicAssetArguments.publicAssetsInIdentityArguments, + isDefaultIdentity: isDefaultIdentity.value); + popBack(result: identityRequest); + } else { + final identityRequest = EditIdentityRequest( + identityId: identity!.id!, + identityRequest: identityAndPublicAssetArguments.identity.toIdentityRequest(), + publicAssetsInIdentityArguments: identityAndPublicAssetArguments.publicAssetsInIdentityArguments, + isDefaultIdentity: isDefaultIdentity.value); + popBack(result: identityRequest); + } + } + + Future<({ + Identity identity, + PublicAssetsInIdentityArguments publicAssetsInIdentityArguments + })> _generateIdentityAndPublicAssetArguments() async { final signatureHtmlText = PlatformInfo.isWeb ? contentHtmlEditor : await _getSignatureHtmlText(); @@ -466,23 +515,10 @@ class IdentityCreatorController extends BaseController with DragDropFileMixin { htmlSignature: Signature(signatureHtmlText), sortOrder: sortOrder); - final generateCreateId = Id(uuid.v1()); - - if (actionType.value == IdentityActionType.create) { - final identityRequest = CreateNewIdentityRequest( - generateCreateId, - newIdentity, - publicAssetsInIdentityArguments: publicAssetsInIdentityArguments, - isDefaultIdentity: isDefaultIdentity.value); - popBack(result: identityRequest); - } else { - final identityRequest = EditIdentityRequest( - identityId: identity!.id!, - identityRequest: newIdentity.toIdentityRequest(), - publicAssetsInIdentityArguments: publicAssetsInIdentityArguments, - isDefaultIdentity: isDefaultIdentity.value); - popBack(result: identityRequest); - } + return ( + identity: newIdentity, + publicAssetsInIdentityArguments: publicAssetsInIdentityArguments + ); } void onCheckboxChanged() { @@ -872,4 +908,34 @@ class IdentityCreatorController extends BaseController with DragDropFileMixin { logError("IdentityCreatorController::_uploadMultipleFilesToPublicAsset: error: $e"); } } + + @override + Future onBeforeUnloadBrowserListener(html.Event event) async { + if (event is html.BeforeUnloadEvent) { + event.preventDefault(); + } + } + + @override + Future onUnloadBrowserListener(html.Event event) => _saveIdentityCacheOnWebAction(); + + @override + Future onBeforeReconnect() => _saveIdentityCacheOnWebAction(); + + Future _saveIdentityCacheOnWebAction() async { + if (accountId != null && session?.username != null) { + final cacheArguments = await _generateIdentityAndPublicAssetArguments(); + final identityCache = IdentityCache( + identity: cacheArguments.identity, + identityActionType: actionType.value, + isDefault: isDefaultIdentity.value, + publicAssetsInIdentityArguments: cacheArguments.publicAssetsInIdentityArguments); + + consumeState(_saveIdentityCacheOnWebInteractor.execute( + accountId!, + session!.username, + identityCache: identityCache + )); + } + } } \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart b/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart index 16245588f..d84b22085 100644 --- a/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart +++ b/lib/features/identity_creator/presentation/model/identity_creator_arguments.dart @@ -4,11 +4,14 @@ 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/identities/identity.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; +import 'package:tmail_ui_user/features/public_asset/domain/model/public_assets_in_identity_arguments.dart'; class IdentityCreatorArguments with EquatableMixin { final AccountId accountId; final Session session; final IdentityActionType actionType; + final PublicAssetsInIdentityArguments? publicAssetsInIdentityArguments; + final bool? isDefault; final Identity? identity; IdentityCreatorArguments( @@ -16,6 +19,8 @@ class IdentityCreatorArguments with EquatableMixin { this.session, { this.identity, + this.publicAssetsInIdentityArguments, + this.isDefault, this.actionType = IdentityActionType.create } ); @@ -25,5 +30,7 @@ class IdentityCreatorArguments with EquatableMixin { accountId, session, identity, + publicAssetsInIdentityArguments, + isDefault, actionType]; } \ No newline at end of file diff --git a/lib/features/public_asset/data/model/public_assets_in_identity_arguments_model.dart b/lib/features/public_asset/data/model/public_assets_in_identity_arguments_model.dart new file mode 100644 index 000000000..2d0f80b68 --- /dev/null +++ b/lib/features/public_asset/data/model/public_assets_in_identity_arguments_model.dart @@ -0,0 +1,28 @@ +import 'package:jmap_dart_client/http/converter/id_converter.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:tmail_ui_user/features/public_asset/domain/model/public_assets_in_identity_arguments.dart'; + +part 'public_assets_in_identity_arguments_model.g.dart'; + +@JsonSerializable(converters: [IdConverter()]) +class PublicAssetsInIdentityArgumentsModel extends PublicAssetsInIdentityArguments { + PublicAssetsInIdentityArgumentsModel({ + required super.htmlSignature, + required super.preExistingPublicAssetIds, + required super.newlyPickedPublicAssetIds}); + + factory PublicAssetsInIdentityArgumentsModel.fromJson(Map json) + => _$PublicAssetsInIdentityArgumentsModelFromJson(json); + + Map toJson() => _$PublicAssetsInIdentityArgumentsModelToJson(this); + + factory PublicAssetsInIdentityArgumentsModel.fromDomain( + PublicAssetsInIdentityArguments publicAssetsInIdentityArguments + ) { + return PublicAssetsInIdentityArgumentsModel( + htmlSignature: publicAssetsInIdentityArguments.htmlSignature, + preExistingPublicAssetIds: publicAssetsInIdentityArguments.preExistingPublicAssetIds, + newlyPickedPublicAssetIds: publicAssetsInIdentityArguments.newlyPickedPublicAssetIds + ); + } +} \ No newline at end of file diff --git a/lib/features/public_asset/presentation/public_asset_controller.dart b/lib/features/public_asset/presentation/public_asset_controller.dart index fdde332e8..7602ad143 100644 --- a/lib/features/public_asset/presentation/public_asset_controller.dart +++ b/lib/features/public_asset/presentation/public_asset_controller.dart @@ -173,6 +173,14 @@ class PublicAssetController extends BaseController { preExistingPublicAssetIds = htmlContent.publicAssetIdsFromHtmlContent; } + void restorePreExistingPublicAssetsFromCache(List publicAssetIds) { + preExistingPublicAssetIds = publicAssetIds; + } + + void restoreNewlyPickedPublicAssetsFromCache(List publicAssetIds) { + newlyPickedPublicAssetIds = publicAssetIds; + } + void uploadFileToBlob(PlatformFile platformFile) { isUploading.value = true; _uploadFileToBlobAction(platformFile); diff --git a/test/features/identity_creator/data/datasource_impl/local_identity_creator_data_source_impl_test.dart b/test/features/identity_creator/data/datasource_impl/local_identity_creator_data_source_impl_test.dart new file mode 100644 index 000000000..c4122a10a --- /dev/null +++ b/test/features/identity_creator/data/datasource_impl/local_identity_creator_data_source_impl_test.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:mockito/annotations.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; +import 'package:tmail_ui_user/features/identity_creator/data/datasource_impl/local_identity_creator_data_source_impl.dart'; +import 'package:tmail_ui_user/features/identity_creator/data/model/identity_cache_model.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/model/identity_cache.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; +import 'package:tmail_ui_user/features/public_asset/domain/model/public_assets_in_identity_arguments.dart'; +import 'package:tmail_ui_user/features/public_asset/presentation/public_asset_controller.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; +import 'package:universal_html/html.dart'; + +import '../../../../fixtures/account_fixtures.dart'; +import '../../../../fixtures/session_fixtures.dart'; +import 'local_identity_creator_data_source_impl_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + final exceptionThrower = MockExceptionThrower(); + final localIdentityCreatorDataSourceImpl = LocalIdentityCreatorDataSourceImpl(exceptionThrower); + final session = SessionFixtures.aliceSession; + final accountId = AccountFixtures.aliceAccountId; + + group('local identity creator data source impl test:', () { + test( + 'should save identity cache as expected', + () async { + // arrange + final identity = Identity(); + const identityActionType = IdentityActionType.create; + const isDefault = true; + final htmlSignature = identity.signatureAsString; + final preExistingPublicAssetIds = []; + final newlyPickedPublicAssetIds = []; + final publicAssetsInIdentityArguments = PublicAssetsInIdentityArguments( + htmlSignature: htmlSignature, + preExistingPublicAssetIds: preExistingPublicAssetIds, + newlyPickedPublicAssetIds: newlyPickedPublicAssetIds, + ); + final identityCache = IdentityCache( + identity: identity, + identityActionType: identityActionType, + isDefault: isDefault, + publicAssetsInIdentityArguments: publicAssetsInIdentityArguments); + + // act + await localIdentityCreatorDataSourceImpl.saveIdentityCacheOnWeb( + accountId, + session.username, + identityCache: identityCache); + final cacheKey = TupleKey( + LocalIdentityCreatorDataSourceImpl.sessionStorageKeyword, + accountId.asString, + session.username.value + ).toString(); + final cache = window.sessionStorage[cacheKey]; + + // assert + expect( + jsonDecode(cache!), + equals(IdentityCacheModel.fromDomain(identityCache).toJson())); + }); + }); +} \ No newline at end of file diff --git a/test/features/identity_creator/presentation/identity_creator_controller_test.dart b/test/features/identity_creator/presentation/identity_creator_controller_test.dart index f591a78a0..3a00dcf15 100644 --- a/test/features/identity_creator/presentation/identity_creator_controller_test.dart +++ b/test/features/identity_creator/presentation/identity_creator_controller_test.dart @@ -5,6 +5,8 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/utils/application_manager.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:dartz/dartz.dart' hide State; import 'package:flutter/widgets.dart' hide State; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; @@ -17,26 +19,34 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/model/identity_cache.dart'; +import 'package:tmail_ui_user/features/identity_creator/domain/usecase/save_identity_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/model/identity_creator_arguments.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/mailbox_creator/domain/state/verify_name_view_state.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.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/model/identity_action_type.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/utils/identity_utils.dart'; +import 'package:tmail_ui_user/features/public_asset/domain/model/public_assets_in_identity_arguments.dart'; import 'package:tmail_ui_user/features/public_asset/domain/usecase/create_public_asset_interactor.dart'; import 'package:tmail_ui_user/features/public_asset/domain/usecase/delete_public_assets_interactor.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; +import 'package:tmail_ui_user/main/universal_import/html_stub.dart'; import 'package:tmail_ui_user/main/utils/toast_manager.dart'; import 'package:uuid/uuid.dart'; import 'package:worker_manager/worker_manager.dart'; @@ -75,6 +85,8 @@ import 'identity_creator_controller_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -84,6 +96,7 @@ void main() { late MockGetAllIdentitiesInteractor mockGetAllIdentitiesInteractor; late MockIdentityUtils mockIdentityUtils; late MockDeletePublicAssetsInteractor deletePublicAssetsInteractor; + late MockSaveIdentityCacheOnWebInteractor mockSaveIdentityCacheOnWebInteractor; late MockCachingManager mockCachingManager; late MockLanguageCacheManager mockLanguageCacheManager; @@ -138,6 +151,7 @@ void main() { mockGetAllIdentitiesInteractor = MockGetAllIdentitiesInteractor(); mockIdentityUtils = MockIdentityUtils(); deletePublicAssetsInteractor = MockDeletePublicAssetsInteractor(); + mockSaveIdentityCacheOnWebInteractor = MockSaveIdentityCacheOnWebInteractor(); Get.put(MockDioClient(), tag: BindingTag.isolateTag); Get.put(MockExecutor()); @@ -149,16 +163,29 @@ void main() { Get.put(MockUploadAttachmentInteractor()); Get.put(MockCreatePublicAssetInteractor()); Get.put(MockHtmlDataSourceImpl(), tag: BindingTag.publicAssetBindingsTag); + Get.put(MockBeforeReconnectManager()); Get.testMode = true; identityCreatorController = IdentityCreatorController( mockVerifyNameInteractor, mockGetAllIdentitiesInteractor, + mockSaveIdentityCacheOnWebInteractor, mockIdentityUtils); }); group("IdentityCreatorController test:", () { + final accountId = AccountId(Id('value')); + final account = Account( + AccountName('value'), + true, + true, + {CapabilityIdentifier.jmapPublicAsset: DefaultCapability({})}); + final session = Session( + {}, + {accountId: account}, + {}, UserName('value'), Uri(), Uri(), Uri(), Uri(), State('value')); + test( 'should call deletePublicAssetsInteractor.execute() ' 'when closeView() is called ' @@ -204,16 +231,6 @@ void main() { 'and user has not picked any image', () { // arrange - final accountId = AccountId(Id('value')); - final account = Account( - AccountName('value'), - true, - true, - {CapabilityIdentifier.jmapPublicAsset: DefaultCapability({})}); - final session = Session( - {}, - {accountId: account}, - {}, UserName('value'), Uri(), Uri(), Uri(), Uri(), State('value')); identityCreatorController.arguments = IdentityCreatorArguments(accountId, session); // act @@ -235,5 +252,105 @@ void main() { publicAssetIds: anyNamed('publicAssetIds'), )); }); + + test( + 'should call saveIdentityCacheOnWebInteractor.execute() ' + 'when screen is reloaded', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + const htmlContent = '

test

'; + const identityName = 'test'; + identityCreatorController.arguments = IdentityCreatorArguments(accountId, session); + when(mockVerifyNameInteractor.execute(any, any)).thenAnswer((_) => Right(VerifyNameViewState())); + + // act + identityCreatorController.onReady(); + expect(identityCreatorController.publicAssetController, isNotNull); + + final context = MockBuildContext(); + identityCreatorController.updateNameIdentity(context, identityName); + identityCreatorController.updateContentHtmlEditor(htmlContent); + + identityCreatorController.onUnloadBrowserListener(Event('')); + await untilCalled(mockSaveIdentityCacheOnWebInteractor.execute( + any, + any, + identityCache: anyNamed('identityCache'), + )); + + // assert + verify(mockSaveIdentityCacheOnWebInteractor.execute( + accountId, + session.username, + identityCache: IdentityCache( + identity: Identity( + name: identityName, + bcc: {}, + replyTo: {}, + htmlSignature: Signature(htmlContent) + ), + identityActionType: IdentityActionType.create, + isDefault: false, + publicAssetsInIdentityArguments: PublicAssetsInIdentityArguments( + htmlSignature: htmlContent, + preExistingPublicAssetIds: [], + newlyPickedPublicAssetIds: [] + ) + ), + )).called(1); + + PlatformInfo.isTestingForWeb = false; + }); + + test( + 'should call saveIdentityCacheOnWebInteractor.execute() ' + 'when session expires', + () async { + // arrange + PlatformInfo.isTestingForWeb = true; + const htmlContent = '

test

'; + const identityName = 'test'; + identityCreatorController.arguments = IdentityCreatorArguments(accountId, session); + when(mockVerifyNameInteractor.execute(any, any)).thenAnswer((_) => Right(VerifyNameViewState())); + + // act + identityCreatorController.onReady(); + expect(identityCreatorController.publicAssetController, isNotNull); + + final context = MockBuildContext(); + identityCreatorController.updateNameIdentity(context, identityName); + identityCreatorController.updateContentHtmlEditor(htmlContent); + + identityCreatorController.onBeforeReconnect(); + await untilCalled(mockSaveIdentityCacheOnWebInteractor.execute( + any, + any, + identityCache: anyNamed('identityCache'), + )); + + // assert + verify(mockSaveIdentityCacheOnWebInteractor.execute( + accountId, + session.username, + identityCache: IdentityCache( + identity: Identity( + name: identityName, + bcc: {}, + replyTo: {}, + htmlSignature: Signature(htmlContent) + ), + identityActionType: IdentityActionType.create, + isDefault: false, + publicAssetsInIdentityArguments: PublicAssetsInIdentityArguments( + htmlSignature: htmlContent, + preExistingPublicAssetIds: [], + newlyPickedPublicAssetIds: [] + ) + ), + )).called(1); + + PlatformInfo.isTestingForWeb = false; + }); }); } \ No newline at end of file