diff --git a/forward/lib/forward/tmail_forward.dart b/forward/lib/forward/tmail_forward.dart index 6952aaa87..df80ccc0f 100644 --- a/forward/lib/forward/tmail_forward.dart +++ b/forward/lib/forward/tmail_forward.dart @@ -9,12 +9,12 @@ part 'tmail_forward.g.dart'; @JsonSerializable(explicitToJson: true, includeIfNull: false) class TMailForward extends Forward { final ForwardId? id; - final bool localCopy; - final Set forwards; + final bool? localCopy; + final Set? forwards; TMailForward({ this.id, - required this.localCopy, - required this.forwards, + this.localCopy, + this.forwards, }); factory TMailForward.fromJson(Map json) => diff --git a/lib/features/manage_account/data/network/forwarding_api.dart b/lib/features/manage_account/data/network/forwarding_api.dart index 93149d01c..2b428595b 100644 --- a/lib/features/manage_account/data/network/forwarding_api.dart +++ b/lib/features/manage_account/data/network/forwarding_api.dart @@ -1,14 +1,18 @@ +import 'package:core/utils/app_logger.dart'; import 'package:forward/forward/forward_id.dart'; import 'package:forward/forward/get/get_forward_method.dart'; import 'package:forward/forward/get/get_forward_response.dart'; import 'package:forward/forward/set/set_forward_method.dart'; +import 'package:forward/forward/set/set_forward_response.dart'; import 'package:forward/forward/tmail_forward.dart'; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/jmap_request.dart'; +import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; import 'package:tmail_ui_user/features/manage_account/domain/exceptions/forward_exception.dart'; -class ForwardingAPI { +class ForwardingAPI with HandleSetErrorMixin { final HttpClient _httpClient; ForwardingAPI(this._httpClient); @@ -31,7 +35,7 @@ class ForwardingAPI { getForwardInvocation.methodCallId, GetForwardResponse.deserialize); - final tMailForwardResult = result?.list.first; + final tMailForwardResult = result?.list.firstOrNull; if (tMailForwardResult == null) { throw NotFoundForwardException(); } @@ -40,14 +44,15 @@ class ForwardingAPI { } Future updateForward(AccountId accountId, TMailForward forward) async { + log('ForwardingAPI::updateForward: ${forward.toJson()}'); final setForwardMethod = SetForwardMethod(accountId) ..addUpdatesSingleton({ ForwardIdSingleton.forwardIdSingleton.id : forward }); final processingInvocation = ProcessingInvocation(); - final requestBuilder = JmapRequestBuilder(_httpClient, processingInvocation) - ..invocation(setForwardMethod); + final requestBuilder = JmapRequestBuilder(_httpClient, processingInvocation); + final setForwardInvocation = requestBuilder.invocation(setForwardMethod); final getForwardMethod = GetForwardMethod(accountId) ..addIds({ForwardIdSingleton.forwardIdSingleton.id}); @@ -58,11 +63,27 @@ class ForwardingAPI { .build() .execute(); + final setForwardResponse = response.parse( + setForwardInvocation.methodCallId, + SetForwardResponse.deserialize, + ); + + final mapErrors = handleSetResponse([setForwardResponse]); + if (mapErrors.isNotEmpty) { + throw SetMethodException(mapErrors); + } + + final updatedForward = setForwardResponse?.updated; + if (updatedForward?.isNotEmpty != true) { + throw UpdateForwardException(); + } + final getForwardResponse = response.parse( getForwardInvocation.methodCallId, - GetForwardResponse.deserialize); + GetForwardResponse.deserialize, + ); - final newForward = getForwardResponse?.list.first; + final newForward = getForwardResponse?.list.firstOrNull; if (newForward == null) { throw NotFoundForwardException(); } diff --git a/lib/features/manage_account/domain/exceptions/forward_exception.dart b/lib/features/manage_account/domain/exceptions/forward_exception.dart index 11d2b7b16..22e3d2931 100644 --- a/lib/features/manage_account/domain/exceptions/forward_exception.dart +++ b/lib/features/manage_account/domain/exceptions/forward_exception.dart @@ -1,5 +1,7 @@ class NotFoundForwardException implements Exception {} +class UpdateForwardException implements Exception {} + class RecipientListIsEmptyException implements Exception {} class RecipientListWithInvalidEmailsException implements Exception {} \ No newline at end of file diff --git a/lib/features/manage_account/domain/model/add_recipients_in_forwarding_request.dart b/lib/features/manage_account/domain/model/add_recipients_in_forwarding_request.dart index 57694b05c..14a41f34f 100644 --- a/lib/features/manage_account/domain/model/add_recipients_in_forwarding_request.dart +++ b/lib/features/manage_account/domain/model/add_recipients_in_forwarding_request.dart @@ -11,11 +11,14 @@ class AddRecipientInForwardingRequest with EquatableMixin { }); TMailForward get newTMailForward { - final newListRecipients = currentForward.forwards - .where((recipient) => !listRecipientAdded.contains(recipient)) - .toSet(); - newListRecipients.addAll(listRecipientAdded); - return currentForward.copyWith(forwards: newListRecipients); + final currentRecipients = currentForward.forwards ?? {}; + + final updatedRecipients = { + ...currentRecipients, + ...listRecipientAdded, + }; + + return currentForward.copyWith(forwards: updatedRecipients); } @override diff --git a/lib/features/manage_account/domain/model/delete_recipient_in_forwarding_request.dart b/lib/features/manage_account/domain/model/delete_recipient_in_forwarding_request.dart index b6ed193a5..91ee0cf19 100644 --- a/lib/features/manage_account/domain/model/delete_recipient_in_forwarding_request.dart +++ b/lib/features/manage_account/domain/model/delete_recipient_in_forwarding_request.dart @@ -11,11 +11,11 @@ class DeleteRecipientInForwardingRequest with EquatableMixin { }); TMailForward get newTMailForward { - final newListRecipients = currentForward.forwards - .where((recipient) => !listRecipientDeleted.contains(recipient)) - .toSet(); + final currentRecipients = currentForward.forwards ?? {}; - return currentForward.copyWith(forwards: newListRecipients); + final updatedRecipients = currentRecipients.difference(listRecipientDeleted.toSet()); + + return currentForward.copyWith(forwards: updatedRecipients); } @override diff --git a/lib/features/manage_account/presentation/extensions/tmail_forward_extension.dart b/lib/features/manage_account/presentation/extensions/tmail_forward_extension.dart index cc9d3e85e..ffd4c31e2 100644 --- a/lib/features/manage_account/presentation/extensions/tmail_forward_extension.dart +++ b/lib/features/manage_account/presentation/extensions/tmail_forward_extension.dart @@ -7,7 +7,7 @@ extension TMailForwardExtension on TMailForward { List get listRecipientForward { return forwards - .map((value) => RecipientForward(EmailAddress(value, value))) - .toList(); + ?.map((value) => RecipientForward(EmailAddress(value, value))) + .toList() ?? []; } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/forward/extensions/handle_error_when_update_forward_fail_extension.dart b/lib/features/manage_account/presentation/forward/extensions/handle_error_when_update_forward_fail_extension.dart new file mode 100644 index 000000000..e537cb450 --- /dev/null +++ b/lib/features/manage_account/presentation/forward/extensions/handle_error_when_update_forward_fail_extension.dart @@ -0,0 +1,40 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/add_recipient_in_forwarding_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/delete_recipient_in_forwarding_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/edit_local_copy_in_forwarding_state.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/forward/forward_controller.dart'; + +extension HandleErrorWhenUpdateForwardFailExtension on ForwardController { + void handleErrorWhenUpdateForwardFail(Failure failure) { + logError( + 'HandleErrorWhenUpdateForwardFailExtension::handleErrorWhenUpdateForwardFail: $failure', + ); + if (failure is AddRecipientsInForwardingFailure) { + _handleAddRecipientsInForwardingFailure(failure); + } else if (failure is DeleteRecipientInForwardingFailure) { + _handleDeleteRecipientInForwardingFailure(failure); + } else if (failure is EditLocalCopyInForwardingFailure) { + _handleEditLocalCopyInForwardingFailure(failure); + } + } + + void _handleAddRecipientsInForwardingFailure( + AddRecipientsInForwardingFailure failure, + ) { + toastManager.showMessageFailure(failure); + } + + void _handleDeleteRecipientInForwardingFailure( + DeleteRecipientInForwardingFailure failure, + ) { + cancelSelectionMode(); + toastManager.showMessageFailure(failure); + } + + void _handleEditLocalCopyInForwardingFailure( + EditLocalCopyInForwardingFailure failure, + ) { + toastManager.showMessageFailure(failure); + } +} diff --git a/lib/features/manage_account/presentation/forward/forward_controller.dart b/lib/features/manage_account/presentation/forward/forward_controller.dart index 3b7a1920b..3c212255d 100644 --- a/lib/features/manage_account/presentation/forward/forward_controller.dart +++ b/lib/features/manage_account/presentation/forward/forward_controller.dart @@ -28,6 +28,7 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_forwar import 'package:tmail_ui_user/features/manage_account/presentation/action/dashboard_setting_action.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/tmail_forward_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/forward/controller/forward_recipient_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/forward/extensions/handle_error_when_update_forward_fail_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/recipient_forward.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -85,7 +86,6 @@ class ForwardController extends BaseController { @override void handleSuccessViewState(Success success) { - super.handleSuccessViewState(success); if (success is GetForwardSuccess) { currentForward.value = success.forward; listRecipientForward.value = currentForward.value!.listRecipientForward; @@ -96,14 +96,19 @@ class ForwardController extends BaseController { _handleAddRecipientsSuccess(success); } else if (success is EditLocalCopyInForwardingSuccess) { _handleEditLocalCopySuccess(success); + } else { + super.handleSuccessViewState(success); } } @override void handleFailureViewState(Failure failure) { - super.handleFailureViewState(failure); - if (failure is DeleteRecipientInForwardingFailure) { - cancelSelectionMode(); + if (failure is AddRecipientsInForwardingFailure || + failure is DeleteRecipientInForwardingFailure || + failure is EditLocalCopyInForwardingFailure) { + handleErrorWhenUpdateForwardFail(failure); + } else { + super.handleFailureViewState(failure); } } @@ -262,7 +267,7 @@ class ForwardController extends BaseController { accountId, EditLocalCopyInForwardingRequest( currentForward: currentForward.value!, - keepLocalCopy: !currentForward.value!.localCopy))); + keepLocalCopy: currentForward.value!.localCopy != true))); } } @@ -270,7 +275,7 @@ class ForwardController extends BaseController { if (currentOverlayContext != null && currentContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, - success.forward.localCopy + success.forward.localCopy == true ? AppLocalizations.of(currentContext!).toastMessageLocalCopyEnable : AppLocalizations.of(currentContext!).toastMessageLocalCopyDisable); } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 39a8ebde2..10975419e 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-05-23T15:09:48.827638", + "@@last_modified": "2025-05-27T01:35:50.697124", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4509,5 +4509,23 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "addRecipientsFailed": "Adding recipient failed.", + "@addRecipientsFailed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "editLocalCopyInForwardFailed": "Edit local copy in forward failed.", + "@editLocalCopyInForwardFailed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteRecipientsFailed": "Delete recipients failed.", + "@deleteRecipientsFailed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index f7cb04e47..5b15fe711 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4745,4 +4745,25 @@ class AppLocalizations { name: 'support', ); } + + String get addRecipientsFailed { + return Intl.message( + 'Adding recipient failed.', + name: 'addRecipientsFailed', + ); + } + + String get editLocalCopyInForwardFailed { + return Intl.message( + 'Edit local copy in forward failed.', + name: 'editLocalCopyInForwardFailed', + ); + } + + String get deleteRecipientsFailed { + return Intl.message( + 'Delete recipients failed.', + name: 'deleteRecipientsFailed', + ); + } } diff --git a/lib/main/utils/toast_manager.dart b/lib/main/utils/toast_manager.dart index 2fac9f69e..9f649048a 100644 --- a/lib/main/utils/toast_manager.dart +++ b/lib/main/utils/toast_manager.dart @@ -8,10 +8,12 @@ import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/utils/app_logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; import 'package:jmap_dart_client/jmap/core/error/method/exception/error_method_response_exception.dart'; import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/email/domain/state/parse_email_by_blob_id_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/preview_email_from_eml_file_state.dart'; @@ -20,6 +22,9 @@ import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart' import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/clear_mailbox_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/add_recipient_in_forwarding_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/delete_recipient_in_forwarding_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/edit_local_copy_in_forwarding_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/export_trace_log_state.dart'; import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_twake_workplace_state.dart'; import 'package:tmail_ui_user/features/starting_page/domain/state/sign_up_twake_workplace_state.dart'; @@ -93,6 +98,10 @@ class ToastManager { message = exception.errorMessage.isNotEmpty ? exception.errorMessage : AppLocalizations.of(currentContext!).emptySpamFolderFailed; + } else if (exception is MethodLevelErrors) { + message = exception.message != null + ? exception.message.toString() + : AppLocalizations.of(currentContext!).emptySpamFolderFailed; } else { message = AppLocalizations.of(currentContext!).emptySpamFolderFailed; } @@ -102,6 +111,10 @@ class ToastManager { message = exception.description?.trim().isNotEmpty == true ? exception.description : AppLocalizations.of(currentContext!).emptyTrashFolderFailed; + } else if (exception is MethodLevelErrors) { + message = exception.message != null + ? exception.message.toString() + : AppLocalizations.of(currentContext!).emptyTrashFolderFailed; } else if (exception is ErrorMethodResponseException) { message = exception.errorMessage.isNotEmpty ? exception.errorMessage @@ -143,6 +156,48 @@ class ToastManager { } else { message = AppLocalizations.of(currentContext!).exportTraceLogFailed; } + } else if (failure is AddRecipientsInForwardingFailure) { + final exception = failure.exception; + if (exception is SetMethodException && exception.mapErrors.isNotEmpty) { + final firstError = exception.mapErrors.values.first; + message = '[${firstError.type.value}] ${firstError.description}'; + } else if (exception is ErrorMethodResponseException && + exception.errorResponse is ErrorMethodResponse) { + final errorResponse = exception.errorResponse as ErrorMethodResponse; + message = '[${errorResponse.type.value}] ${errorResponse.description}'; + } else if (exception is MethodLevelErrors) { + message = '[${exception.type.value}] ${exception.message}'; + } else { + message = AppLocalizations.of(currentContext!).addRecipientsFailed; + } + } else if (failure is EditLocalCopyInForwardingFailure) { + final exception = failure.exception; + if (exception is SetMethodException && exception.mapErrors.isNotEmpty) { + final firstError = exception.mapErrors.values.first; + message = '[${firstError.type.value}] ${firstError.description}'; + } else if (exception is ErrorMethodResponseException && + exception.errorResponse is ErrorMethodResponse) { + final errorResponse = exception.errorResponse as ErrorMethodResponse; + message = '[${errorResponse.type.value}] ${errorResponse.description}'; + } else if (exception is MethodLevelErrors) { + message = '[${exception.type.value}] ${exception.message}'; + } else { + message = AppLocalizations.of(currentContext!).editLocalCopyInForwardFailed; + } + } else if (failure is DeleteRecipientInForwardingFailure) { + final exception = failure.exception; + if (exception is SetMethodException && exception.mapErrors.isNotEmpty) { + final firstError = exception.mapErrors.values.first; + message = '[${firstError.type.value}] ${firstError.description}'; + } else if (exception is ErrorMethodResponseException && + exception.errorResponse is ErrorMethodResponse) { + final errorResponse = exception.errorResponse as ErrorMethodResponse; + message = '[${errorResponse.type.value}] ${errorResponse.description}'; + } else if (exception is MethodLevelErrors) { + message = '[${exception.type.value}] ${exception.message}'; + } else { + message = AppLocalizations.of(currentContext!).deleteRecipientsFailed; + } } if (message?.isNotEmpty == true) {