diff --git a/Mail/Views/Bottom sheets/RestoreEmailsView.swift b/Mail/Views/Bottom sheets/RestoreEmailsView.swift index 0a7ff7de9..71764957c 100644 --- a/Mail/Views/Bottom sheets/RestoreEmailsView.swift +++ b/Mail/Views/Bottom sheets/RestoreEmailsView.swift @@ -76,7 +76,7 @@ struct RestoreEmailsView: View { Task { await tryOrDisplayError { try await mailboxManager.apiFetcher.restoreBackup(mailbox: mailboxManager.mailbox, date: selectedDate) - await IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarSuccessfulRestoration) + IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarRestorationLaunched) } } } diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift index 3918ccbe8..405f75975 100644 --- a/Mail/Views/Onboarding/OnboardingView.swift +++ b/Mail/Views/Onboarding/OnboardingView.swift @@ -18,8 +18,8 @@ import AuthenticationServices import InfomaniakCoreUI -import InfomaniakDI import InfomaniakCreateAccount +import InfomaniakDI import InfomaniakLogin import Lottie import MailCore @@ -71,7 +71,7 @@ struct Slide: Identifiable { class LoginHandler: InfomaniakLoginDelegate, ObservableObject { @LazyInjectService var loginService: InfomaniakLoginable @LazyInjectService var matomo: MatomoUtils - + @Published var isLoading = false @Published var isPresentingErrorAlert = false var sceneDelegate: SceneDelegate? @@ -129,7 +129,7 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { _ = try await AccountManager.instance.createAndSetCurrentAccount(code: code, codeVerifier: verifier) sceneDelegate?.showMainView() UIApplication.shared.registerForRemoteNotifications() - } catch MailError.noMailbox { + } catch let error as MailError where error == MailError.noMailbox { sceneDelegate?.showNoMailboxView() } catch { if let previousAccount = previousAccount { diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index b1c2beed8..b29ecf6b3 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -108,7 +108,7 @@ struct SplitView: View { if let tappedNotificationThread = tappedNotificationMessage?.originalThread { navigationStore.threadPath = [tappedNotificationThread] } else { - IKSnackBar.showSnackBar(message: MailError.messageNotFound.errorDescription ?? "") + IKSnackBar.showSnackBar(message: MailError.messageNotFound.errorDescription) } } .onAppear { diff --git a/MailCore/API/ApiError.swift b/MailCore/API/ApiError.swift deleted file mode 100644 index 21ebe495a..000000000 --- a/MailCore/API/ApiError.swift +++ /dev/null @@ -1,187 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation - -enum ApiErrorCode: String { - // General - case notAuthorized = "not_authorized" - - // Folder - case folderUnableToCreate = "folder__unable_to_create" - case folderUnableToUpdate = "folder__unable_to_update" - case folderUnableToDelete = "folder__unable_to_delete" - case folderUnableToFlush = "folder__unable_to_flush" - case protectedFolder = "folder__protected_folder" - case folderUnableToMoveInSub = "folder__unable_to_move_folder_in_its_sub_folders" - case destinationFolderAlreadyExists = "folder__destination_folder_already_exists" - case rootDestinationNotExists = "folder__root_destination_not_exists" - case folderAlreadyExists = "folder__destination_already_exists" - case folderNotExists = "folder__not_exists" - - // Mail - case moveDestinationNotFound = "mail__move_destination_folder_not_found" - case cannotConnectToIMAPServer = "mail__cannot_connect_to_server" - case IMAPAuthFailed = "mail__imap_authentication_failed" - case IMAPUnableToParseResponse = "mail__imap_unable_to_parse_response" - case IMAPConnectionTimedOut = "mail__imap_connection_timedout" - case cannotConnectToSMTPServer = "mail__cannot_connect_to_smtp_server" - case SMTPAuthFailed = "mail__smtp_authentication_failed" - case messageNotFound = "mail__message_not_found" - case messageAttachmentNotFound = "mail__message_attachment_not_found" - case unableToUndoMoveAction = "mail__unable_to_undo_move_action" - case unableToMoveEmails = "mail__unable_to_move_emails" - - // Draft - case draftAttachmentNotFound = "draft__attachment_not_found" - case draftNotFound = "draft__not_found" - case draftMessageNotFound = "draft__message_not_found" - case draftTooManyRecipients = "draft__to_many_recipients" - case draftMaxAttachmentsSizeReached = "draft__max_attachments_size_reached" - case draftNeedAtLeastOneRecipient = "draft__need_at_least_one_recipient_to_be_sent" - case draftAlreadyScheduledOrSent = "draft__cannot_modify_scheduled_or_already_sent_message" - case draftCannotCancelNonScheduledMessage = "draft__cannot_cancel_non_scheduled_message" - case draftCannotForwardMoreThanOneMessageInline = "draft__cannot_forward_more_than_one_message_inline" - case draftCannotMoveScheduledMessage = "draft__cannot_move_scheduled_message" - - // Send - case sendFromRefused = "send__server_refused_from" - case sendRecipientRefused = "send__server_refused_all_recipients" - case sendLimitExceeded = "send__server_rate_limit_exceeded" - case sendUnknownError = "send__server_unknown_error" - case sendDailyLimitReached = "send__server_daily_limit_reached" - case sendSpamRejected = "send__spam_rejected" - case sendSenderMismatch = "send__sender_mismatch" - - // Attachment - case attachmentNotValid = "attachment__not_valid" - case attachmentNotFound = "attachment__not_found" - case attachmentCannotRender = "attachment__cannot_render" - case attachmentRenderError = "attachment__error_while_render" - case attachmentMissingFilenameOrMimeType = "attachment__missing_filename_or_mimetype" - case attachmentUploadIncorrect = "attachment__incorrect_disposition" - case attachmentUploadContentIdNotValid = "attachment__content_id_not_valid" - case attachmentAddFromDriveFailed = "attachment__add_attachment_from_drive_fail" - case attachmentStoreToDriveFailed = "attachment__store_to_drive_fail" - - // Message - case messageUidIsNotValid = "message__uid_is_not_valid" - - var localizedDescription: String { - switch self { - case .notAuthorized: - return "Not authorized" - case .folderUnableToCreate: - return "Unable to create folder" - case .folderUnableToUpdate: - return "Unable to update folder" - case .folderUnableToDelete: - return "Unable to delete folder" - case .folderUnableToFlush: - return "Unable to flush folder" - case .protectedFolder: - return "Protected folder" - case .folderUnableToMoveInSub: - return "Unable to move folder in its sub folders" - case .destinationFolderAlreadyExists: - return "Destination folder already exists" - case .rootDestinationNotExists: - return "Root destination does not exist" - case .folderAlreadyExists: - return "Destination already exists" - case .folderNotExists: - return "Folder does not exist" - case .moveDestinationNotFound: - return "Move destination folder not found" - case .cannotConnectToIMAPServer: - return "Cannot connect to IMAP server" - case .IMAPAuthFailed: - return "IMAP authentication failed" - case .IMAPUnableToParseResponse: - return "Unable to parse IMAP response" - case .IMAPConnectionTimedOut: - return "IMAP connection timed out" - case .cannotConnectToSMTPServer: - return "Cannot connect to SMTP server" - case .SMTPAuthFailed: - return "SMTP authentication failed" - case .messageNotFound: - return "Message not found" - case .messageAttachmentNotFound: - return "Message attachment not found" - case .unableToUndoMoveAction: - return "Unable to undo move action" - case .unableToMoveEmails: - return "Unable to move emails" - case .draftAttachmentNotFound: - return "Attachment not found" - case .draftNotFound: - return "Draft not found" - case .draftMessageNotFound: - return "Message not found" - case .draftTooManyRecipients: - return "Too many recipients" - case .draftMaxAttachmentsSizeReached: - return "Max attachments size reached" - case .draftNeedAtLeastOneRecipient: - return "Draft needs at least one recipient to be sent" - case .draftAlreadyScheduledOrSent: - return "Cannot modify scheduled or already sent message" - case .draftCannotCancelNonScheduledMessage: - return "Cannot cancel non scheduled message" - case .draftCannotForwardMoreThanOneMessageInline: - return "Cannot forward more than one message inline" - case .draftCannotMoveScheduledMessage: - return "Cannot move scheduled message" - case .sendFromRefused: - return "Server refused from" - case .sendRecipientRefused: - return "Server refused all recipients" - case .sendLimitExceeded: - return "Rate limit exceeded" - case .sendUnknownError: - return "Unknown server error" - case .sendDailyLimitReached: - return "Daily limit reached" - case .sendSpamRejected: - return "Spam rejected" - case .sendSenderMismatch: - return "Sender mismatch" - case .attachmentNotValid: - return "Attachment not valid" - case .attachmentNotFound: - return "Attachment not found" - case .attachmentCannotRender: - return "Attachment cannot render" - case .attachmentRenderError: - return "Attachment render error" - case .attachmentMissingFilenameOrMimeType: - return "Attachment missing filename or mime type" - case .attachmentUploadIncorrect: - return "Attachment incorrect disposition" - case .attachmentUploadContentIdNotValid: - return "Attachment content ID not valid" - case .attachmentAddFromDriveFailed: - return "Add attachment from drive failed" - case .attachmentStoreToDriveFailed: - return "Store attachment to drive failed" - case .messageUidIsNotValid: - return "Message UID is not valid" - } - } -} diff --git a/MailCore/API/MailApiError.swift b/MailCore/API/MailApiError.swift new file mode 100644 index 000000000..4d3c39b02 --- /dev/null +++ b/MailCore/API/MailApiError.swift @@ -0,0 +1,117 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation +import MailResources + +class MailApiError: MailError { + static let allErrors: [MailApiError] = [ + // General + MailApiError(code: "not_authorized"), + + // Folder + MailApiError(code: "folder__unable_to_create"), + MailApiError(code: "folder__unable_to_update"), + MailApiError(code: "folder__unable_to_delete"), + MailApiError(code: "folder__unable_to_flush"), + MailApiError(code: "folder__protected_folder"), + MailApiError(code: "folder__unable_to_move_folder_in_its_sub_folders"), + MailApiError(code: "folder__destination_folder_already_exists"), + MailApiError(code: "folder__root_destination_not_exists"), + MailApiError( + code: "folder__destination_already_exists", + localizedDescription: MailResourcesStrings.Localizable.errorNewFolderAlreadyExists, + shouldDisplay: true + ), + MailApiError(code: "folder__not_exists", + localizedDescription: MailResourcesStrings.Localizable.errorFolderNotFound, + shouldDisplay: true), + + // Mail + MailApiError(code: "mail__move_destination_folder_not_found"), + MailApiError(code: "mail__cannot_connect_to_server"), + MailApiError(code: "mail__imap_authentication_failed"), + MailApiError(code: "mail__imap_unable_to_parse_response"), + MailApiError(code: "mail__imap_connection_timedout"), + MailApiError(code: "mail__cannot_connect_to_smtp_server"), + MailApiError(code: "mail__smtp_authentication_failed"), + MailApiError(code: "mail__message_not_found"), + MailApiError(code: "mail__message_attachment_not_found"), + MailApiError(code: "mail__unable_to_undo_move_action"), + MailApiError(code: "mail__unable_to_move_emails"), + + // Draft + MailApiError(code: "draft__attachment_not_found"), + MailApiError(code: "draft__not_found"), + MailApiError(code: "draft__message_not_found"), + MailApiError( + code: "draft__to_many_recipients", + localizedDescription: MailResourcesStrings.Localizable.tooManyRecipients, + shouldDisplay: true + ), + MailApiError(code: "draft__max_attachments_size_reached"), + MailApiError( + code: "draft__need_at_least_one_recipient_to_be_sent", + localizedDescription: MailResourcesStrings.Localizable.errorAtLeastOneRecipient, + shouldDisplay: true + ), + MailApiError( + code: "draft__cannot_modify_scheduled_or_already_sent_message", + localizedDescription: MailResourcesStrings.Localizable.errorEditScheduledMessage, + shouldDisplay: true + ), + MailApiError(code: "draft__cannot_cancel_non_scheduled_message"), + MailApiError(code: "draft__cannot_forward_more_than_one_message_inline"), + MailApiError(code: "draft__cannot_move_scheduled_message"), + + // Send + MailApiError(code: "send__server_refused_from"), + MailApiError(code: "send__server_refused_all_recipients", + localizedDescription: MailResourcesStrings.Localizable.errorRefusedRecipients, + shouldDisplay: true), + MailApiError(code: "send__server_rate_limit_exceeded", + localizedDescription: MailResourcesStrings.Localizable.errorSendLimitExceeded, + shouldDisplay: true), + MailApiError(code: "send__server_unknown_error"), + MailApiError(code: "send__server_daily_limit_reached"), + MailApiError(code: "send__spam_rejected"), + MailApiError(code: "send__sender_mismatch"), + + // Attachment + MailApiError(code: "attachment__not_valid"), + MailApiError(code: "attachment__not_found"), + MailApiError(code: "attachment__cannot_render"), + MailApiError(code: "attachment__error_while_render"), + MailApiError(code: "attachment__missing_filename_or_mimetype"), + MailApiError(code: "attachment__incorrect_disposition"), + MailApiError(code: "attachment__content_id_not_valid"), + MailApiError(code: "attachment__add_attachment_from_drive_fail"), + MailApiError(code: "attachment__store_to_drive_fail"), + + // Message + MailApiError(code: "message__uid_is_not_valid") + ] + + static func mailApiErrorFromCode(_ code: String) -> MailApiError? { + return allErrors.first { $0.code == code } + } + + static func mailApiErrorWithFallback(apiErrorCode: String) -> MailError { + return mailApiErrorFromCode(apiErrorCode) ?? MailError.unknownError + } +} diff --git a/MailCore/API/MailApiFetcher.swift b/MailCore/API/MailApiFetcher.swift index 5f34c6cbc..82943822c 100644 --- a/MailCore/API/MailApiFetcher.swift +++ b/MailCore/API/MailApiFetcher.swift @@ -43,9 +43,9 @@ public class MailApiFetcher: ApiFetcher { do { return try await super.perform(request: request) } catch InfomaniakError.apiError(let apiError) { - throw MailError.apiError(apiError) + throw MailApiError.mailApiErrorWithFallback(apiErrorCode: apiError.code) } catch InfomaniakError.serverError(statusCode: let statusCode) { - throw MailError.serverError(statusCode: statusCode) + throw MailServerError(httpStatus: statusCode) } catch { if let afError = error.asAFError { if case .responseSerializationFailed(let reason) = afError, diff --git a/MailCore/API/MailError.swift b/MailCore/API/MailError.swift index 4e5941a2f..3bbb61364 100644 --- a/MailCore/API/MailError.swift +++ b/MailCore/API/MailError.swift @@ -23,64 +23,65 @@ import MailResources extension ApiError: CustomStringConvertible {} -struct AFErrorWithContext: LocalizedError { +class AFErrorWithContext: MailError { let request: DataRequest let afError: AFError - public var errorDescription: String? { - return MailError.unknownError.errorDescription + init(request: DataRequest, afError: AFError) { + self.request = request + self.afError = afError + super.init(code: "afErrorWithContext", shouldDisplay: false) } } -public enum MailError: LocalizedError { - case apiError(ApiError) - case serverError(statusCode: Int) - case noToken - case resourceError - case unknownError - case unknownToken - case noMailbox - case messageNotFound - case folderNotFound - case addressBookNotFound - case contactNotFound - case attachmentsSizeLimitReached +public class MailError: LocalizedError { + public let code: String + public let errorDescription: String + public let shouldDisplay: Bool - public var errorDescription: String? { - switch self { - case .apiError(let apiError): - if let code = ApiErrorCode(rawValue: apiError.code) { - return code.localizedDescription - } - return apiError.description - case .noToken: - return "No API token" - case .resourceError: - return "Resource error" - case .unknownError: - return "Unknown error" - case .serverError: - return "Server error" - case .unknownToken: - return "Unknown token" - case .noMailbox: - return "No Mailbox" - case .folderNotFound: - return "Folder not found" - case .addressBookNotFound: - return "Address Book not found" - case .contactNotFound: - return "Contact not found" - case .messageNotFound: - return "Message not found" - case .attachmentsSizeLimitReached: - return MailResourcesStrings.Localizable.attachmentFileLimitReached - } + init(code: String, + localizedDescription: String = MailResourcesStrings.Localizable.errorUnknown, + shouldDisplay: Bool = false) { + self.code = code + errorDescription = localizedDescription + self.shouldDisplay = shouldDisplay } + + public static let unknownError = MailError(code: "unknownError", shouldDisplay: true) + public static let noToken = MailError(code: "noToken", shouldDisplay: true) + public static let resourceError = MailError(code: "resourceError", shouldDisplay: true) + public static let unknownToken = MailError(code: "unknownToken", shouldDisplay: true) + public static let noMailbox = MailError(code: "noMailbox") + public static let folderNotFound = MailError(code: "folderNotFound", + localizedDescription: MailResourcesStrings.Localizable.errorFolderNotFound, + shouldDisplay: true) + public static let addressBookNotFound = MailError(code: "addressBookNotFound", shouldDisplay: true) + public static let contactNotFound = MailError(code: "contactNotFound", shouldDisplay: true) + public static let messageNotFound = MailError(code: "messageNotFound", + localizedDescription: MailResourcesStrings.Localizable.errorMessageNotFound, + shouldDisplay: true) + public static let attachmentsSizeLimitReached = MailError(code: "attachmentsSizeLimitReached", + localizedDescription: MailResourcesStrings.Localizable + .attachmentFileLimitReached, + shouldDisplay: true) } extension MailError: Identifiable { public var id: String { - return errorDescription ?? UUID().uuidString + return code + } +} + +extension MailError: Equatable { + public static func == (lhs: MailError, rhs: MailError) -> Bool { + return lhs.code == rhs.code + } +} + +public class MailServerError: MailError { + let httpStatus: Int + init(httpStatus: Int) { + self.httpStatus = httpStatus + super.init(code: "serverError") } } diff --git a/MailCore/Utils/Error+Extension.swift b/MailCore/Utils/Error+Extension.swift index 22a0e7d5b..d69dc43df 100644 --- a/MailCore/Utils/Error+Extension.swift +++ b/MailCore/Utils/Error+Extension.swift @@ -16,19 +16,16 @@ along with this program. If not, see . */ +import CocoaLumberjackSwift import Foundation import InfomaniakCoreUI +import Sentry public func tryOrDisplayError(_ body: () throws -> Void) { do { try body() } catch { - if error.shouldDisplay { - Task.detached { - await IKSnackBar.showSnackBar(message: error.localizedDescription) - } - } - print("Error: \(error)") + displayErrorIfNeeded(error: error) } } @@ -36,12 +33,30 @@ public func tryOrDisplayError(_ body: () async throws -> Void) async { do { try await body() } catch { + displayErrorIfNeeded(error: error) + } +} + +private func displayErrorIfNeeded(error: Error) { + if let error = error as? MailError { if error.shouldDisplay { Task.detached { - await IKSnackBar.showSnackBar(message: error.localizedDescription) + await IKSnackBar.showSnackBar(message: error.errorDescription) } + } else { + SentrySDK.capture(message: "Encountered error that we didn't display to the user") { scope in + scope.setContext( + value: ["Code": error.code, "Raw": error], + key: "Error" + ) + } + } + DDLogError("MailError: \(error)") + } else if error.shouldDisplay { + Task.detached { + await IKSnackBar.showSnackBar(message: error.localizedDescription) } - print("Error: \(error)") + DDLogError("Error: \(error)") } } @@ -50,7 +65,7 @@ public extension Error { switch asAFError { case .explicitlyCancelled: return false - case let .sessionTaskFailed(error): + case .sessionTaskFailed(let error): return (error as NSError).code != NSURLErrorNotConnectedToInternet default: return true diff --git a/MailResources/Localizable/de.lproj/Localizable.strings b/MailResources/Localizable/de.lproj/Localizable.strings index 9071dd613..b2216640d 100644 Binary files a/MailResources/Localizable/de.lproj/Localizable.strings and b/MailResources/Localizable/de.lproj/Localizable.strings differ diff --git a/MailResources/Localizable/en.lproj/Localizable.strings b/MailResources/Localizable/en.lproj/Localizable.strings index 20561d2c4..0d40821d4 100644 Binary files a/MailResources/Localizable/en.lproj/Localizable.strings and b/MailResources/Localizable/en.lproj/Localizable.strings differ diff --git a/MailResources/Localizable/es.lproj/Localizable.strings b/MailResources/Localizable/es.lproj/Localizable.strings index 20be060ac..79943c364 100644 Binary files a/MailResources/Localizable/es.lproj/Localizable.strings and b/MailResources/Localizable/es.lproj/Localizable.strings differ diff --git a/MailResources/Localizable/fr.lproj/Localizable.strings b/MailResources/Localizable/fr.lproj/Localizable.strings index aae046dda..23d30659d 100644 Binary files a/MailResources/Localizable/fr.lproj/Localizable.strings and b/MailResources/Localizable/fr.lproj/Localizable.strings differ diff --git a/MailResources/Localizable/it.lproj/Localizable.strings b/MailResources/Localizable/it.lproj/Localizable.strings index 71a9f077d..bd1ebe7c8 100644 Binary files a/MailResources/Localizable/it.lproj/Localizable.strings and b/MailResources/Localizable/it.lproj/Localizable.strings differ