From 9f1edab92a4aaac73d94bed7db3f8e0a6e073c22 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Thu, 22 Jan 2026 02:24:10 +0200 Subject: [PATCH 1/3] Fix displaying container messages, improve file opening --- .../Mimetype/Decoder/MimeTypeDecoder.swift | 11 ++++-- .../Mimetype/Parser/XMLParserHandler.swift | 36 +++++++++++-------- .../UtilsLib/Model/ContainerType.swift | 2 +- .../Parser/XMLParserHandlerTests.swift | 22 ++++++------ .../Supporting files/Localizable.xcstrings | 2 +- .../DataFileBottomSheetActions.swift | 4 +-- .../Container/Signing/SigningView.swift | 1 + .../Signing/SmartId/SmartIdView.swift | 17 ++++----- .../ViewModel/FileOpeningViewModel.swift | 2 +- .../Protocols/SigningViewModelProtocol.swift | 1 + RIADigiDoc/ViewModel/SigningViewModel.swift | 4 +++ .../ViewModel/FileOpeningViewModelTests.swift | 22 ++++++------ 12 files changed, 73 insertions(+), 51 deletions(-) diff --git a/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Decoder/MimeTypeDecoder.swift b/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Decoder/MimeTypeDecoder.swift index 1aaea182..344539a3 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Decoder/MimeTypeDecoder.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Decoder/MimeTypeDecoder.swift @@ -25,8 +25,15 @@ public struct MimeTypeDecoder: MimeTypeDecoderProtocol { await withCheckedContinuation { continuation in let parser = XMLParser(data: xmlData) let handler = XMLParserHandler(continuation: continuation) + parser.delegate = handler - parser.parse() - } ? .ddoc : .none + + let success = parser.parse() + + if !success { + handler.finishIfNeeded() + } + } } + } diff --git a/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Parser/XMLParserHandler.swift b/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Parser/XMLParserHandler.swift index 86b4e8ed..ef9fe54a 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Parser/XMLParserHandler.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Parser/XMLParserHandler.swift @@ -19,11 +19,13 @@ import Foundation -class XMLParserHandler: NSObject, XMLParserDelegate { - private var continuation: CheckedContinuation? +final class XMLParserHandler: NSObject, XMLParserDelegate { + + private let continuation: CheckedContinuation + private var didResume = false private var foundElement = false - init(continuation: CheckedContinuation) { + init(continuation: CheckedContinuation) { self.continuation = continuation } @@ -36,23 +38,27 @@ class XMLParserHandler: NSObject, XMLParserDelegate { ) { if elementName == "SignedDoc", attributeDict["format"] == "DIGIDOC-XML" { foundElement = true - continuation?.resume(returning: true) - continuation = nil parser.abortParsing() } } - func parserDidEndDocument(_: XMLParser) { - if continuation != nil { - continuation?.resume(returning: foundElement) - continuation = nil - } + // swiftlint:disable:next unused_parameter + func parserDidEndDocument(_ parser: XMLParser) { + resume(foundElement ? .ddoc : .none) } - func parser(_: XMLParser, parseErrorOccurred _: Error) { - if continuation != nil { - continuation?.resume(returning: foundElement) - continuation = nil - } + // swiftlint:disable:next unused_parameter + func parser(_ parser: XMLParser, parseErrorOccurred error: Error) { + resume(foundElement ? .ddoc : .none) + } + + func finishIfNeeded() { + resume(foundElement ? .ddoc : .none) + } + + private func resume(_ value: ContainerType) { + guard !didResume else { return } + didResume = true + continuation.resume(returning: value) } } diff --git a/Modules/UtilsLib/Sources/UtilsLib/Model/ContainerType.swift b/Modules/UtilsLib/Sources/UtilsLib/Model/ContainerType.swift index 6163970a..ab72ac6e 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/Model/ContainerType.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/Model/ContainerType.swift @@ -19,7 +19,7 @@ import Foundation -public enum ContainerType { +public enum ContainerType: Sendable { case none case asice case cdoc diff --git a/Modules/UtilsLib/Tests/UtilsLibTests/Parser/XMLParserHandlerTests.swift b/Modules/UtilsLib/Tests/UtilsLibTests/Parser/XMLParserHandlerTests.swift index 58ecb36e..5470aefb 100644 --- a/Modules/UtilsLib/Tests/UtilsLibTests/Parser/XMLParserHandlerTests.swift +++ b/Modules/UtilsLib/Tests/UtilsLibTests/Parser/XMLParserHandlerTests.swift @@ -34,54 +34,54 @@ struct XMLParserHandlerTests { """ let data = Data(xml.utf8) - let result = await withCheckedContinuation { (continuation: CheckedContinuation) in + let result = await withCheckedContinuation { (continuation: CheckedContinuation) in let handler = XMLParserHandler(continuation: continuation) let parser = XMLParser(data: data) parser.delegate = handler parser.parse() } - #expect(result == true) + #expect(result == .ddoc) } @Test - func xmlParserHandler_parser_returnFalseWithWrongSignedDocFormat() async { + func xmlParserHandler_parser_returnNoneResultWithWrongSignedDocFormat() async { let xml = """ """ let data = Data(xml.utf8) - let result = await withCheckedContinuation { (continuation: CheckedContinuation) in + let result = await withCheckedContinuation { (continuation: CheckedContinuation) in let handler = XMLParserHandler(continuation: continuation) let parser = XMLParser(data: data) parser.delegate = handler parser.parse() } - #expect(result == false) + #expect(result == .none) } @Test - func xmlParserHandler_parser_returnFalseWithNoSignedDocTag() async { + func xmlParserHandler_parser_returnNoneResultWithNoSignedDocTag() async { let xml = """ """ let data = Data(xml.utf8) - let result = await withCheckedContinuation { (continuation: CheckedContinuation) in + let result = await withCheckedContinuation { (continuation: CheckedContinuation) in let handler = XMLParserHandler(continuation: continuation) let parser = XMLParser(data: data) parser.delegate = handler parser.parse() } - #expect(result == false) + #expect(result == .none) } @Test - func xmlParserHandler_parser_returnsFalseWhenXMLIsMalformed() async { + func xmlParserHandler_parser_returnsNoneResultWhenXMLIsMalformed() async { let xml = """ ) in + let result = await withCheckedContinuation { (continuation: CheckedContinuation) in let handler = XMLParserHandler(continuation: continuation) let parser = XMLParser(data: data) parser.delegate = handler parser.parse() } - #expect(result == false) + #expect(result == .none) } } diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index b7b460c5..ebe3b1ed 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -5860,7 +5860,7 @@ "et" : { "stringUnit" : { "state" : "translated", - "value" : "Eemalda konteiner" + "value" : "Eemalda ümbrik" } } } diff --git a/RIADigiDoc/UI/Component/Bottom Sheet/DataFileBottomSheetActions.swift b/RIADigiDoc/UI/Component/Bottom Sheet/DataFileBottomSheetActions.swift index c20eaa47..a7572608 100644 --- a/RIADigiDoc/UI/Component/Bottom Sheet/DataFileBottomSheetActions.swift +++ b/RIADigiDoc/UI/Component/Bottom Sheet/DataFileBottomSheetActions.swift @@ -19,8 +19,8 @@ struct DataFileBottomSheetActions { static func actions( - showOpenFileButton: Bool = false, - showSaveFileButton: Bool = false, + showOpenFileButton: Bool = true, + showSaveFileButton: Bool = true, showRemoveFileButton: Bool = false, onOpenFileButtonClick: @escaping () -> Void, onSaveFileButtonClick: @escaping () -> Void, diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index 383f47b4..d3a4a46e 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -419,6 +419,7 @@ struct SigningView: View { Toast.show( languageSettings.localized(error.key, [error.args.joined(separator: ", ")]) ) + viewModel.resetErrorMessage() } } diff --git a/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift b/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift index 212d84ca..42a0d155 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift @@ -128,6 +128,14 @@ struct SmartIdView: View { } } } + .onChange(of: viewModel.smartIdMessageKey, { _, newValue in + if let messageKey = newValue, !messageKey.isEmpty { + let extraArguments = viewModel.smartIdAlertMessageExtraArguments + Toast.show( + languageSettings.localized(messageKey, extraArguments) + ) + } + }) .fullScreenCover(isPresented: $showRoleView) { RoleView( onComplete: { roles, city, state, country, zipCode in @@ -189,18 +197,11 @@ struct SmartIdView: View { guard let container = updatedContainer else { cancelSigning() isSigning = false - if let messageKey = viewModel.smartIdMessageKey, - !messageKey.isEmpty { - let extraArguments = viewModel.smartIdAlertMessageExtraArguments - Toast.show( - languageSettings.localized(messageKey, extraArguments) - ) - } - return } cancelSigning() + isSigning = false onSuccess(container) dismiss() diff --git a/RIADigiDoc/ViewModel/FileOpeningViewModel.swift b/RIADigiDoc/ViewModel/FileOpeningViewModel.swift index 569ed1de..3439c9f1 100644 --- a/RIADigiDoc/ViewModel/FileOpeningViewModel.swift +++ b/RIADigiDoc/ViewModel/FileOpeningViewModel.swift @@ -130,7 +130,7 @@ class FileOpeningViewModel: FileOpeningViewModelProtocol, Loggable { func showFileAddedMessage() async -> Bool { let container = sharedContainerViewModel.currentContainer() as? any SignedContainerProtocol - return await container?.getSignatures().isEmpty ?? false + return await !(container?.isExistingContainer() ?? true) } func addedFilesCount() -> Int { diff --git a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift index 578b495e..773b88ea 100644 --- a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift @@ -42,4 +42,5 @@ public protocol SigningViewModelProtocol: Sendable { func removeDataFile(_ dataFile: DataFileWrapper) async func isSignatureAdded() -> Bool func removeLastOpenedXattr(from url: URL) + func resetErrorMessage() } diff --git a/RIADigiDoc/ViewModel/SigningViewModel.swift b/RIADigiDoc/ViewModel/SigningViewModel.swift index c4fead1c..dd8f5374 100644 --- a/RIADigiDoc/ViewModel/SigningViewModel.swift +++ b/RIADigiDoc/ViewModel/SigningViewModel.swift @@ -523,6 +523,10 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { } } + func resetErrorMessage() { + errorMessage = nil + } + private func openNestedContainer(fileURL: URL, isSivaConfirmed: Bool) async throws { let container = try await fileOpeningService .openOrCreateContainer(dataFiles: [fileURL], isSivaConfirmed: isSivaConfirmed) diff --git a/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift b/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift index 87fa4a62..84ea0264 100644 --- a/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift @@ -187,17 +187,17 @@ struct FileOpeningViewModelTests { @Test func handleSivaConfirmation_successWithNonSivaContainer() async throws { - let container = SignedContainerProtocolMock() - container.getContainerMimetypeHandler = { Constants.MimeType.Pdf } + let mockContainer = SignedContainerProtocolMock() + mockContainer.getContainerMimetypeHandler = { Constants.MimeType.Pdf } mockFileOpeningRepository.openOrCreateContainerHandler = { _, isSivaConfirmed in #expect(isSivaConfirmed) - return container + return mockContainer } await viewModel.handleSivaConfirmation() - let rawContainerFile = await container.getRawContainerFile() + let rawContainerFile = await mockContainer.getRawContainerFile() #expect(mockSharedContainerViewModel.setAddedFilesCountCallCount == 1) if let count = mockSharedContainerViewModel.setAddedFilesCountArgValues.first { @@ -431,13 +431,14 @@ struct FileOpeningViewModelTests { @Test func showFileAddedMessage_returnFalseWhenContainerIsSigned() async { - let container = SignedContainerProtocolMock() - container.getSignaturesHandler = {[ + let mockContainer = SignedContainerProtocolMock() + mockContainer.getSignaturesHandler = {[ MockSignatureWrapper.mockSignatureWrapper(signatureId: "1"), MockSignatureWrapper.mockSignatureWrapper(signatureId: "2") ]} - mockSharedContainerViewModel.currentContainerHandler = { container } + mockSharedContainerViewModel.currentContainerHandler = { mockContainer } + mockContainer.isExistingContainerHandler = { true } let showFileAddedMessage = await viewModel.showFileAddedMessage() @@ -447,9 +448,10 @@ struct FileOpeningViewModelTests { @Test func showFileAddedMessage_returnTrueWhenContainerIsNotSigned() async { - let container = SignedContainerProtocolMock() - container.getSignaturesHandler = { [] } - mockSharedContainerViewModel.currentContainerHandler = { container } + let mockContainer = SignedContainerProtocolMock() + mockContainer.getSignaturesHandler = { [] } + mockSharedContainerViewModel.currentContainerHandler = { mockContainer } + mockContainer.isExistingContainerHandler = { false } let showFileAddedMessage = await viewModel.showFileAddedMessage() From e32816c15646b5d85ae77afac9b6aebef2741c8a Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Thu, 29 Jan 2026 05:12:27 +0200 Subject: [PATCH 2/3] Add message queue and fix duplicate file message --- .../Domain/Container/ContainerWrapper.swift | 12 ++- .../Domain/Models/ErrorDetail.swift | 49 +++++++++--- .../Errors/DigiDocErrorTests.swift | 4 +- .../Models/ErrorDetailTests.swift | 11 ++- .../SignedContainerTests.swift | 2 +- RIADigiDoc.xcodeproj/project.pbxproj | 1 - .../ToastItem.swift} | 6 +- RIADigiDoc/Domain/Model/ToastMessage.swift | 9 +-- .../Supporting files/Localizable.xcstrings | 18 +++++ .../Container/Signing/SigningView.swift | 7 ++ RIADigiDoc/UI/Component/Toast/Toast.swift | 7 +- .../UI/Component/Toast/ToastController.swift | 31 +++---- .../UI/Component/Toast/ToastQueue.swift | 49 ++++++++++++ RIADigiDoc/ViewModel/EncryptViewModel.swift | 46 +++++------ .../ViewModel/FileOpeningViewModel.swift | 6 +- .../Protocols/SigningViewModelProtocol.swift | 1 + RIADigiDoc/ViewModel/SigningViewModel.swift | 80 +++++++++++-------- .../ViewModel/SigningViewModelTests.swift | 28 +++---- 18 files changed, 245 insertions(+), 122 deletions(-) rename RIADigiDoc/Domain/Model/{Error/ErrorMessage.swift => Toast/ToastItem.swift} (90%) create mode 100644 RIADigiDoc/UI/Component/Toast/ToastQueue.swift diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift index 821d0b6c..246fed74 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift @@ -158,13 +158,16 @@ public actor ContainerWrapper: ContainerWrapperProtocol, Loggable { let duplicatePrefix = "Document with same file name" let duplicateCount = errors.filter { $0.localizedDescription.hasPrefix(duplicatePrefix) }.count - let totalCount = errors.count + + let failedCount = nsError.userInfo["failedFileCount"] as? Int ?? 0 + let totalCount = nsError.userInfo["totalFileCount"] as? Int ?? dataFilesPaths.count if duplicateCount == totalCount && totalCount > 1 { throw DigiDocError.addingFilesToContainerFailed( ErrorDetail(message: "Multiple documents already exist", code: 4, userInfo: [ - "totalFileCount": String(totalCount), - "duplicateFileCount": String(duplicateCount) + "totalFileCount": totalCount, + "failedFileCount": failedCount, + "duplicateFileCount": duplicateCount ]) ) } else { @@ -175,7 +178,8 @@ public actor ContainerWrapper: ContainerWrapperProtocol, Loggable { throw DigiDocError.addingFilesToContainerFailed( ErrorDetail( - nsError: nsError + nsError: nsError, + extraInfo: ["duplicateFileCount": duplicateCount] ) ) } diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/ErrorDetail.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/ErrorDetail.swift index 01342b69..6e0866a8 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/ErrorDetail.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/ErrorDetail.swift @@ -23,9 +23,9 @@ import LibdigidocLibObjC public struct ErrorDetail: Sendable { public let message: String public let code: Int - public let userInfo: [String: String] + public let userInfo: [String: Sendable] - public init(message: String = "", code: Int = 0, userInfo: [String: String] = [:]) { + public init(message: String = "", code: Int = 0, userInfo: [String: Sendable] = [:]) { self.message = message self.code = code self.userInfo = userInfo @@ -34,14 +34,13 @@ public struct ErrorDetail: Sendable { init(nsError: NSError) { self.message = nsError.localizedDescription self.code = nsError.code - self.userInfo = ErrorDetail - .convertUserInfoToStringDictionary(nsError.userInfo) + self.userInfo = ErrorDetail.extractInfo(from: nsError) } - init(nsError: NSError, extraInfo: [String: String]) { + init(nsError: NSError, extraInfo: [String: Sendable]) { self.message = nsError.localizedDescription self.code = nsError.code - self.userInfo = ErrorDetail.convertUserInfoToStringDictionary(nsError.userInfo) + self.userInfo = ErrorDetail.extractInfo(from: nsError) .merging(extraInfo) { (_, combined) in combined } } @@ -53,9 +52,41 @@ public struct ErrorDetail: Sendable { """ } - private static func convertUserInfoToStringDictionary(_ userInfo: [String: Any]) -> [String: String] { - userInfo.mapValues { value in - String(describing: value) + private static func extractInfo(from error: NSError) -> [String: Sendable] { + var dict: [String: Sendable] = [:] + + for (key, value) in error.userInfo { + dict[key] = String(describing: value) + } + + dict[NSLocalizedDescriptionKey] = error.localizedDescription + + if let failedFileCount = error.userInfo["failedFileCount"] as? Int, failedFileCount > 0 { + dict["failedFileCount"] = failedFileCount } + + if let totalFileCount = error.userInfo["totalFileCount"] as? Int, totalFileCount > 0 { + dict["totalFileCount"] = totalFileCount + } + + if let causes = error.userInfo["causes"] as? [String: Any], + let errors = causes["errors"] as? [NSError], + let firstError = errors.first, + let subCauses = firstError.userInfo["causes"] as? [String: Any], + let file = subCauses["fileName"] as? String, !file.isEmpty { + + dict["fileName"] = file + } + + if let causes = error.userInfo["causes"] as? [String: Any], + let errors = causes["errors"] as? [NSError], + let firstError = errors.first, + let subCauses = firstError.userInfo["causes"] as? [String: Any], + let ex = subCauses["exceptions"] as? [Any], !ex.isEmpty { + + dict["exceptions"] = ex.map { "\($0)" } + } + + return dict } } diff --git a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Errors/DigiDocErrorTests.swift b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Errors/DigiDocErrorTests.swift index b3821de7..ad4a851a 100644 --- a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Errors/DigiDocErrorTests.swift +++ b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Errors/DigiDocErrorTests.swift @@ -33,7 +33,7 @@ final class DigiDocErrorTests { #expect(errorDetail.message == retrievedDetail.message) #expect(errorDetail.code == retrievedDetail.code) - #expect(retrievedDetail.userInfo == ["key": "value"]) + #expect(retrievedDetail.userInfo as? [String : String] == ["key": "value"]) } @Test @@ -44,7 +44,7 @@ final class DigiDocErrorTests { #expect(retrievedDetail.message == "Libdigidocpp is already initialized") #expect(retrievedDetail.code == 0) - #expect(retrievedDetail.userInfo == [:]) + #expect(retrievedDetail.userInfo.isEmpty) } @Test diff --git a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Models/ErrorDetailTests.swift b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Models/ErrorDetailTests.swift index 1fae02e9..da611c9b 100644 --- a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Models/ErrorDetailTests.swift +++ b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Models/ErrorDetailTests.swift @@ -43,7 +43,7 @@ final class ErrorDetailsTests { #expect(message == errorDetail.message) #expect(code == errorDetail.code) - #expect(userInfo == errorDetail.userInfo) + #expect(userInfo == errorDetail.userInfo as? [String: String]) } @Test @@ -58,7 +58,12 @@ final class ErrorDetailsTests { #expect(nsError.localizedDescription == errorDetail.message) #expect(nsError.code == errorDetail.code) - #expect(errorDetail.userInfo == ["key": "value", NSLocalizedDescriptionKey: "Test NSError message"]) + #expect( + errorDetail.userInfo as? [String : String] == [ + "key": "value", + NSLocalizedDescriptionKey: "Test NSError message" + ] + ) } @Test @@ -74,7 +79,7 @@ final class ErrorDetailsTests { #expect(nsError.localizedDescription == errorDetail.message) #expect(nsError.code == errorDetail.code) - #expect(errorDetail.userInfo == [ + #expect(errorDetail.userInfo as? [String : String] == [ "key": "value", NSLocalizedDescriptionKey: "Test NSError message", "extraKey": "extraValue" diff --git a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift index e9876f9e..b15f7a64 100644 --- a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift +++ b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift @@ -153,7 +153,7 @@ final class SignedContainerTests { notExistingContainerUrl .deletingPathExtension() .appendingPathExtension(Constants.Extension.Asice).lastPathComponent == errorDetail - .userInfo["fileName"] + .userInfo["fileName"] as? String ) default: Issue.record("Unexpected error: \(error.localizedDescription)") diff --git a/RIADigiDoc.xcodeproj/project.pbxproj b/RIADigiDoc.xcodeproj/project.pbxproj index 2356d133..c8d24640 100644 --- a/RIADigiDoc.xcodeproj/project.pbxproj +++ b/RIADigiDoc.xcodeproj/project.pbxproj @@ -143,7 +143,6 @@ Domain/Model/EncryptionServerInfo.swift, Domain/Model/EncryptionServerOption.swift, Domain/Model/EncryptionServerOptionId.swift, - Domain/Model/Error/ErrorMessage.swift, Domain/Model/Error/FileOpeningErrors.swift, "Domain/Model/Error/My eID/MyEidCodeChangeError.swift", Domain/Model/FileItem.swift, diff --git a/RIADigiDoc/Domain/Model/Error/ErrorMessage.swift b/RIADigiDoc/Domain/Model/Toast/ToastItem.swift similarity index 90% rename from RIADigiDoc/Domain/Model/Error/ErrorMessage.swift rename to RIADigiDoc/Domain/Model/Toast/ToastItem.swift index 5f0d62dc..7cbe628b 100644 --- a/RIADigiDoc/Domain/Model/Error/ErrorMessage.swift +++ b/RIADigiDoc/Domain/Model/Toast/ToastItem.swift @@ -19,7 +19,7 @@ import Foundation -struct ErrorMessage: Sendable, Equatable { - let key: String - let args: [String] +struct ToastItem: Sendable { + let message: String + let duration: TimeInterval } diff --git a/RIADigiDoc/Domain/Model/ToastMessage.swift b/RIADigiDoc/Domain/Model/ToastMessage.swift index 72fc29ca..599b07db 100644 --- a/RIADigiDoc/Domain/Model/ToastMessage.swift +++ b/RIADigiDoc/Domain/Model/ToastMessage.swift @@ -19,12 +19,11 @@ import Foundation -struct ToastMessage: Identifiable { - let id = UUID() - let key: String? - let args: [CVarArg] +struct ToastMessage: Sendable, Equatable { + let key: String + let args: [String] - init(key: String?, args: [CVarArg] = []) { + init(key: String, args: [String] = []) { self.key = key self.args = args } diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index ebe3b1ed..e905dfcc 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -4552,6 +4552,24 @@ } } }, + "Multiple documents added" : { + "comment" : "When trying to add multiple data files that are added, but not some other files", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ files added successfully" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ faili edukalt lisatud" + } + } + } + }, "Multiple documents already exist" : { "comment" : "When trying to add multiple data files that already exist in the container", "extractionState" : "manual", diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index d3a4a46e..bb34a51a 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -421,6 +421,13 @@ struct SigningView: View { ) viewModel.resetErrorMessage() } + .onChange(of: viewModel.successMessage) { _, message in + guard let message else { return } + Toast.show( + languageSettings.localized(message.key, [message.args.joined(separator: ", ")]) + ) + viewModel.resetSuccessMessage() + } } private func updateSignAndEncryptButtonVisibility() async { diff --git a/RIADigiDoc/UI/Component/Toast/Toast.swift b/RIADigiDoc/UI/Component/Toast/Toast.swift index 03da4e1e..09e0af25 100644 --- a/RIADigiDoc/UI/Component/Toast/Toast.swift +++ b/RIADigiDoc/UI/Component/Toast/Toast.swift @@ -21,8 +21,11 @@ import SwiftUI struct Toast { static func show(_ message: String, duration: TimeInterval = 5.0) { - Task { @MainActor in - ToastController.shared.show(message: message, duration: duration) + Task { + await ToastQueue.shared.enqueue( + message: message, + duration: duration + ) } } } diff --git a/RIADigiDoc/UI/Component/Toast/ToastController.swift b/RIADigiDoc/UI/Component/Toast/ToastController.swift index cb2d00ba..a3b8d679 100644 --- a/RIADigiDoc/UI/Component/Toast/ToastController.swift +++ b/RIADigiDoc/UI/Component/Toast/ToastController.swift @@ -27,30 +27,25 @@ final class ToastController { var message: String? var isVisible = false - private var dismissTask: Task? + private let showAnimation = Animation.interpolatingSpring(stiffness: 300, damping: 25) - func show(message: String, duration: TimeInterval) { - guard !isVisible else { return } - self.message = message + private let hideAnimation = Animation.easeInOut(duration: 0.3) - withAnimation(.interpolatingSpring(stiffness: 300, damping: 25)) { + func present(_ toast: ToastItem) async { + message = toast.message + + withAnimation(showAnimation) { isVisible = true } - dismissTask?.cancel() - dismissTask = Task { - try? await Task.sleep(for: .seconds(duration)) - - await MainActor.run { - withAnimation(.easeInOut(duration: 0.3)) { - self.isVisible = false - } + try? await Task.sleep(for: .seconds(toast.duration)) - Task { - try? await Task.sleep(for: .seconds(0.3)) - self.message = nil - } - } + withAnimation(hideAnimation) { + isVisible = false } + + try? await Task.sleep(for: .seconds(0.3)) + + message = nil } } diff --git a/RIADigiDoc/UI/Component/Toast/ToastQueue.swift b/RIADigiDoc/UI/Component/Toast/ToastQueue.swift new file mode 100644 index 00000000..c3268050 --- /dev/null +++ b/RIADigiDoc/UI/Component/Toast/ToastQueue.swift @@ -0,0 +1,49 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation + +actor ToastQueue { + static let shared = ToastQueue() + + private var queue: [ToastItem] = [] + private var isPresenting = false + + func enqueue(message: String, duration: TimeInterval) { + queue.append(ToastItem(message: message, duration: duration)) + processQueueIfNeeded() + } + + private func processQueueIfNeeded() { + guard !isPresenting, let next = queue.first else { return } + + isPresenting = true + queue.removeFirst() + + Task { + await ToastController.shared.present(next) + toastDidFinish() + } + } + + private func toastDidFinish() { + isPresenting = false + processQueueIfNeeded() + } +} diff --git a/RIADigiDoc/ViewModel/EncryptViewModel.swift b/RIADigiDoc/ViewModel/EncryptViewModel.swift index 0526cb79..bbc40415 100644 --- a/RIADigiDoc/ViewModel/EncryptViewModel.swift +++ b/RIADigiDoc/ViewModel/EncryptViewModel.swift @@ -39,7 +39,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { var isShowingFileSaver = false var showRecipientRemoveButton = false var isLastDataFileRemoved = false - private(set) var errorMessage: ErrorMessage? + private(set) var errorMessage: ToastMessage? private let sharedContainerViewModel: SharedContainerViewModelProtocol private let fileOpeningService: FileOpeningServiceProtocol @@ -158,7 +158,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { await cryptoContainer?.addDataFiles(files) EncryptViewModel.logger().debug("Added data files to container") - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: files.count == 1 ? "File successfully added" : "Files successfully added", args: [] ) @@ -193,12 +193,12 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { switch cryptoError { case .addingFilesToContainerFailed(let detail): let fileName = detail.userInfo["fileName"] ?? "" - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: detail.message, args: [fileName] ) default: - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "General error", args: [] ) @@ -207,24 +207,24 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { case let fileError as FileOpeningError: switch fileError { case .invalidFileSize: - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "Invalid file size", args: [] ) case .noDataFiles: - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "Could not load selected files", args: [] ) default: - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "General error", args: [] ) } default: - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "General error", args: [] ) @@ -256,19 +256,19 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { if let cryptoError = error as? CryptoError { switch cryptoError { case .containerCreationFailed(let detail): - errorMessage = ErrorMessage(key: detail.message, args: []) + errorMessage = ToastMessage(key: detail.message, args: []) default: - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } return } if let nsError = error as NSError? { - errorMessage = ErrorMessage(key: nsError.localizedDescription, args: []) + errorMessage = ToastMessage(key: nsError.localizedDescription, args: []) return } - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } @discardableResult @@ -281,15 +281,15 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { switch cryptoError { case .containerRenamingFailed(let errorDetail), .containerSavingFailed(let errorDetail): - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "Failed to rename file", args: [errorDetail.userInfo["fileName"] ?? ""] ) default: - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } } else { - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } return nil } @@ -332,13 +332,13 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { // TODO: Open signed container files } catch { EncryptViewModel.logger().error("Failed to open nested container: \(error)") - errorMessage = ErrorMessage(key: "Failed to open container", args: [dataFile.lastPathComponent]) + errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.lastPathComponent]) } } else { previewFile = fileURL } case .failure: - errorMessage = ErrorMessage(key: "Failed to open file", args: [dataFile.lastPathComponent]) + errorMessage = ToastMessage(key: "Failed to open file", args: [dataFile.lastPathComponent]) } } @@ -351,7 +351,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { isShowingFileSaver = true case .failure: - errorMessage = ErrorMessage(key: "Failed to save file", args: [dataFile.lastPathComponent]) + errorMessage = ToastMessage(key: "Failed to save file", args: [dataFile.lastPathComponent]) isShowingFileSaver = false } } @@ -363,7 +363,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { case .success(let fileURL): return await sivaRepository.isSivaConfirmationNeeded(files: [fileURL]) case .failure: - errorMessage = ErrorMessage(key: "Failed to open container", args: [dataFile.lastPathComponent]) + errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.lastPathComponent]) return false } } @@ -509,7 +509,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { EncryptViewModel.logger().error( "Unable to remove signature from container. CryptoContainer or containerURL is nil" ) - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "Failed to remove recipient from container", args: [] ) @@ -521,7 +521,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { await loadContainerData(cryptoContainer: container) } catch { EncryptViewModel.logger().error("Unable to remove signature from container. \(error)") - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "Failed to remove signature from container", args: [] ) @@ -534,7 +534,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { EncryptViewModel.logger().error( "Unable to remove file from container. CryptoContainer or containerURL is nil" ) - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "Failed to remove file from container", args: [dataFile.lastPathComponent] ) @@ -552,7 +552,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { return } catch { EncryptViewModel.logger().error("Unable to remove file from container. \(error)") - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "Failed to remove file from container", args: [dataFile.lastPathComponent] ) diff --git a/RIADigiDoc/ViewModel/FileOpeningViewModel.swift b/RIADigiDoc/ViewModel/FileOpeningViewModel.swift index 3439c9f1..2233eca2 100644 --- a/RIADigiDoc/ViewModel/FileOpeningViewModel.swift +++ b/RIADigiDoc/ViewModel/FileOpeningViewModel.swift @@ -183,11 +183,11 @@ class FileOpeningViewModel: FileOpeningViewModelProtocol, Loggable { case .containerCreationFailed(let errorDetail), .containerOpeningFailed(let errorDetail), .containerSavingFailed(let errorDetail): - return ToastMessage(key: "Failed to open container", args: [errorDetail.userInfo["fileName"] ?? ""]) + return ToastMessage(key: "Failed to open container", args: [errorDetail.userInfo["fileName"] as? String ?? ""]) case .addingFilesToContainerFailed(let errorDetail): - return ToastMessage(key: "Failed to open file", args: [errorDetail.userInfo["fileName"] ?? ""]) + return ToastMessage(key: "Failed to open file", args: [errorDetail.userInfo["fileName"] as? String ?? ""]) case .containerDataFileSavingFailed(let errorDetail): - return ToastMessage(key: "Failed to save file", args: [errorDetail.userInfo["fileName"] ?? ""]) + return ToastMessage(key: "Failed to save file", args: [errorDetail.userInfo["fileName"] as? String ?? ""]) case .alreadyInitialized: return ToastMessage(key: "Libdigidocpp is already initialized") default: diff --git a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift index 773b88ea..3ccc0240 100644 --- a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift @@ -43,4 +43,5 @@ public protocol SigningViewModelProtocol: Sendable { func isSignatureAdded() -> Bool func removeLastOpenedXattr(from url: URL) func resetErrorMessage() + func resetSuccessMessage() } diff --git a/RIADigiDoc/ViewModel/SigningViewModel.swift b/RIADigiDoc/ViewModel/SigningViewModel.swift index dd8f5374..6ba763f6 100644 --- a/RIADigiDoc/ViewModel/SigningViewModel.swift +++ b/RIADigiDoc/ViewModel/SigningViewModel.swift @@ -43,7 +43,8 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { var isXadesContainer = false var isLastDataFileRemoved = false private(set) var containerNotifications: [ContainerNotificationType] = [] - private(set) var errorMessage: ErrorMessage? + private(set) var errorMessage: ToastMessage? + private(set) var successMessage: ToastMessage? private let sharedContainerViewModel: SharedContainerViewModelProtocol private let fileOpeningService: FileOpeningServiceProtocol @@ -185,7 +186,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { do { let updatedContainer = try await signedContainer?.addDataFiles(files, to: container) SigningViewModel.logger().debug("Added data files to container") - errorMessage = ErrorMessage( + successMessage = ToastMessage( key: files.count == 1 ? "File successfully added" : "Files successfully added", args: [] ) @@ -234,58 +235,67 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { case let digiDocError as DigiDocError: switch digiDocError { case .addingFilesToContainerFailed(let detail): - let fileName = detail.userInfo["fileName"] ?? "" - errorMessage = ErrorMessage(key: detail.message, args: [fileName]) + let fileName = detail.userInfo["fileName"] as? String ?? "" + errorMessage = ToastMessage(key: detail.message, args: [fileName]) default: - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } case let fileError as FileOpeningError: switch fileError { case .invalidFileSize: - errorMessage = ErrorMessage(key: "Invalid file size", args: []) + errorMessage = ToastMessage(key: "Invalid file size", args: []) case .noDataFiles: - errorMessage = ErrorMessage(key: "Could not load selected files", args: []) + errorMessage = ToastMessage(key: "Could not load selected files", args: []) default: - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } default: - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } } private func handleAddFilesError(_ error: Error, container: URL) async { SigningViewModel.logger().error("Unable to add data files to container: \(error.localizedDescription)") - var totalFilesCount = 0 + var totalFileCount = 0 var failedFileCount = 0 var duplicateFileCount = 0 guard let digiDocError = error as? DigiDocError else { - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) return } switch digiDocError { case .addingFilesToContainerFailed(let errorDetail): - totalFilesCount = Int(errorDetail.userInfo["totalFileCount"] ?? "0") ?? 0 - failedFileCount = Int(errorDetail.userInfo["failedFileCount"] ?? "0") ?? 0 - duplicateFileCount = Int(errorDetail.userInfo["duplicateFileCount"] ?? "0") ?? 0 + totalFileCount = Int(errorDetail.userInfo["totalFileCount"] as? Int ?? 0) + failedFileCount = Int(errorDetail.userInfo["failedFileCount"] as? Int ?? 0) + duplicateFileCount = Int(errorDetail.userInfo["duplicateFileCount"] as? Int ?? 0) if duplicateFileCount > 1 { - errorMessage = ErrorMessage(key: errorDetail.message, args: [String(duplicateFileCount)]) + errorMessage = ToastMessage(key: errorDetail.message, args: [String(duplicateFileCount)]) + + } else if duplicateFileCount == 1 { + if let fileName = errorDetail.userInfo["fileName"] as? String { + errorMessage = ToastMessage(key: "Document already exists", args: [fileName]) + } else { + errorMessage = ToastMessage(key: errorDetail.message, args: [String(failedFileCount)]) + } } else { - errorMessage = ErrorMessage(key: errorDetail.message, args: [String(failedFileCount)]) + errorMessage = ToastMessage(key: errorDetail.message, args: [String(failedFileCount)]) } default: - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } // Update container when at least one file has been added to container - if totalFilesCount > failedFileCount { + if totalFileCount > failedFileCount { await refreshContainer(with: container) + let successfulFilesCount = totalFileCount - failedFileCount + successMessage = ToastMessage(key: "Multiple documents added", args: [String(successfulFilesCount)]) } } @@ -297,7 +307,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { ) await loadContainerData(signedContainer: updatedContainer) } catch { - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } } @@ -315,15 +325,15 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { switch digiDocError { case .containerRenamingFailed(let errorDetail), .containerSavingFailed(let errorDetail): - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "Failed to rename file", - args: [errorDetail.userInfo["fileName"] ?? ""] + args: [errorDetail.userInfo["fileName"] as? String ?? ""] ) default: - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } } else { - errorMessage = ErrorMessage(key: "General error", args: []) + errorMessage = ToastMessage(key: "General error", args: []) } return nil } @@ -365,7 +375,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { try await openNestedContainer(fileURL: fileURL, isSivaConfirmed: isSivaConfirmed) } catch { SigningViewModel.logger().error("Failed to open nested container: \(error)") - errorMessage = ErrorMessage(key: "Failed to open container", args: [dataFile.fileName]) + errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.fileName]) if error.localizedDescription.contains("Online validation disabled") { SigningViewModel.logger().error( "Unable to open container '\([dataFile.fileName])'. Sending to SiVa not allowed." @@ -373,14 +383,14 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { errorMessage = nil } else { SigningViewModel.logger().error("Failed to open nested container: \(error)") - errorMessage = ErrorMessage(key: "Failed to open container", args: [dataFile.fileName]) + errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.fileName]) } } } else { previewFile = fileURL } case .failure: - errorMessage = ErrorMessage(key: "Failed to open file", args: [dataFile.fileName]) + errorMessage = ToastMessage(key: "Failed to open file", args: [dataFile.fileName]) } } @@ -393,7 +403,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { isShowingFileSaver = true case .failure: - errorMessage = ErrorMessage(key: "Failed to save file", args: [dataFile.fileName]) + errorMessage = ToastMessage(key: "Failed to save file", args: [dataFile.fileName]) isShowingFileSaver = false } } @@ -405,7 +415,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { case .success(let fileURL): return await sivaRepository.isSivaConfirmationNeeded(files: [fileURL]) case .failure: - errorMessage = ErrorMessage(key: "Failed to open container", args: [dataFile.fileName]) + errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.fileName]) return false } } @@ -469,7 +479,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { SigningViewModel.logger().error( "Unable to remove signature from container. SignedContainer or containerURL is nil" ) - errorMessage = ErrorMessage(key: "Failed to remove signature from container", args: []) + errorMessage = ToastMessage(key: "Failed to remove signature from container", args: []) return } @@ -478,7 +488,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { await loadContainerData(signedContainer: container) } catch { SigningViewModel.logger().error("Unable to remove signature from container. \(error)") - errorMessage = ErrorMessage(key: "Failed to remove signature from container", args: []) + errorMessage = ToastMessage(key: "Failed to remove signature from container", args: []) return } } @@ -488,7 +498,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { SigningViewModel.logger().error( "Unable to remove file from container. SignedContainer or containerURL is nil" ) - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "Failed to remove file from container", args: [dataFile.fileName] ) @@ -499,7 +509,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { SigningViewModel.logger().error( "Unable to remove file from container. File not found in container" ) - errorMessage = ErrorMessage(key: "Failed to remove file from container", args: [dataFile.fileName]) + errorMessage = ToastMessage(key: "Failed to remove file from container", args: [dataFile.fileName]) return } @@ -515,7 +525,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { return } catch { SigningViewModel.logger().error("Unable to remove file from container. \(error)") - errorMessage = ErrorMessage( + errorMessage = ToastMessage( key: "Failed to remove file from container", args: [dataFile.fileName] ) @@ -527,6 +537,10 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { errorMessage = nil } + func resetSuccessMessage() { + successMessage = nil + } + private func openNestedContainer(fileURL: URL, isSivaConfirmed: Bool) async throws { let container = try await fileOpeningService .openOrCreateContainer(dataFiles: [fileURL], isSivaConfirmed: isSivaConfirmed) diff --git a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift index 2fbdc022..4921d38b 100644 --- a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift @@ -972,7 +972,7 @@ struct SigningViewModelTests: Loggable { let errorMessage = viewModel.errorMessage - #expect(errorMessage == ErrorMessage(key: "Failed to remove signature from container", args: [])) + #expect(errorMessage == ToastMessage(key: "Failed to remove signature from container", args: [])) } @Test @@ -990,7 +990,7 @@ struct SigningViewModelTests: Loggable { let errorMessage = viewModel.errorMessage - #expect(errorMessage == ErrorMessage(key: "Failed to remove signature from container", args: [])) + #expect(errorMessage == ToastMessage(key: "Failed to remove signature from container", args: [])) } @Test @@ -1012,7 +1012,7 @@ struct SigningViewModelTests: Loggable { let errorMessage = viewModel.errorMessage - #expect(errorMessage == ErrorMessage(key: "Failed to remove signature from container", args: [])) + #expect(errorMessage == ToastMessage(key: "Failed to remove signature from container", args: [])) } @Test @@ -1057,7 +1057,7 @@ struct SigningViewModelTests: Loggable { let errorMessage = viewModel.errorMessage #expect( - errorMessage == ErrorMessage( + errorMessage == ToastMessage( key: "Failed to remove file from container", args: [mockDataFile.fileName] ) @@ -1081,7 +1081,7 @@ struct SigningViewModelTests: Loggable { let errorMessage = viewModel.errorMessage #expect( - errorMessage == ErrorMessage( + errorMessage == ToastMessage( key: "Failed to remove file from container", args: [mockDataFile.fileName] ) @@ -1113,7 +1113,7 @@ struct SigningViewModelTests: Loggable { let errorMessage = viewModel.errorMessage #expect( - errorMessage == ErrorMessage( + errorMessage == ToastMessage( key: "Failed to remove file from container", args: [mockDataFile.fileName] ) @@ -1153,7 +1153,7 @@ struct SigningViewModelTests: Loggable { to: containerFile ) - #expect(viewModel.errorMessage == ErrorMessage(key: "File successfully added", args: [])) + #expect(viewModel.successMessage == ToastMessage(key: "File successfully added", args: [])) await #expect(updatedMockSignedContainer.getDataFiles().count == 2) } @@ -1192,7 +1192,7 @@ struct SigningViewModelTests: Loggable { to: containerFile ) - #expect(viewModel.errorMessage == ErrorMessage(key: "Files successfully added", args: [])) + #expect(viewModel.successMessage == ToastMessage(key: "Files successfully added", args: [])) await #expect(updatedMockSignedContainer.getDataFiles().count == 3) } @@ -1217,7 +1217,7 @@ struct SigningViewModelTests: Loggable { to: containerFile ) - #expect(viewModel.errorMessage == ErrorMessage(key: "Invalid file size", args: [])) + #expect(viewModel.errorMessage == ToastMessage(key: "Invalid file size", args: [])) #expect(mockSignedContainer.addDataFilesCallCount == 0) } @@ -1237,7 +1237,7 @@ struct SigningViewModelTests: Loggable { await viewModel.addDataFiles([], to: containerFile) - #expect(viewModel.errorMessage == ErrorMessage(key: "Could not load selected files", args: [])) + #expect(viewModel.errorMessage == ToastMessage(key: "Could not load selected files", args: [])) #expect(mockSignedContainer.addDataFilesCallCount == 0) } @@ -1272,7 +1272,7 @@ struct SigningViewModelTests: Loggable { to: containerFile ) - #expect(viewModel.errorMessage == ErrorMessage(key: "Document already exists", args: [mockFileName])) + #expect(viewModel.errorMessage == ToastMessage(key: "Document already exists", args: [mockFileName])) await #expect(mockSignedContainer.getDataFiles().count == 2) } @@ -1329,9 +1329,7 @@ struct SigningViewModelTests: Loggable { testFile, testFile2, testFile3 ], to: containerFile) - print(signedContainer) - - #expect(viewModel.errorMessage == ErrorMessage(key: "Multiple documents already exist", args: ["2"])) + #expect(viewModel.errorMessage == ToastMessage(key: "Could not add files", args: ["2"])) #expect(viewModel.dataFiles.count == 3) } @@ -1386,7 +1384,7 @@ struct SigningViewModelTests: Loggable { testFile, testFile2 ], to: containerFile) - #expect(viewModel.errorMessage == ErrorMessage(key: "Multiple documents already exist", args: ["2"])) + #expect(viewModel.errorMessage == ToastMessage(key: "Multiple documents already exist", args: ["2"])) #expect(viewModel.dataFiles.count == 2) } } From 8851cc519ca3f38052ba8bc0162b96d4099a1b95 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 30 Jan 2026 18:18:47 +0200 Subject: [PATCH 3/3] Fix message for multiple duplicate documents --- .../Supporting files/Localizable.xcstrings | 21 ++++++++++++++++--- RIADigiDoc/ViewModel/SigningViewModel.swift | 20 +++++++++++------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index e905dfcc..b4d5b20f 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -1,9 +1,6 @@ { "sourceLanguage" : "en", "strings" : { - "" : { - - }, "Add file" : { "comment" : "Home screen bottom sheet action", "extractionState" : "manual", @@ -6994,6 +6991,24 @@ } } }, + "Single document added" : { + "comment" : "When trying to add multiple data files, one is added, but not other files", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 file added successfully" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 fail edukalt lisatud" + } + } + } + }, "Siva message" : { "comment" : "SiVa message on file opening", "extractionState" : "manual", diff --git a/RIADigiDoc/ViewModel/SigningViewModel.swift b/RIADigiDoc/ViewModel/SigningViewModel.swift index 6ba763f6..0d767d5c 100644 --- a/RIADigiDoc/ViewModel/SigningViewModel.swift +++ b/RIADigiDoc/ViewModel/SigningViewModel.swift @@ -275,8 +275,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { duplicateFileCount = Int(errorDetail.userInfo["duplicateFileCount"] as? Int ?? 0) if duplicateFileCount > 1 { - errorMessage = ToastMessage(key: errorDetail.message, args: [String(duplicateFileCount)]) - + errorMessage = ToastMessage(key: "Multiple documents already exist", args: [String(duplicateFileCount)]) } else if duplicateFileCount == 1 { if let fileName = errorDetail.userInfo["fileName"] as? String { errorMessage = ToastMessage(key: "Document already exists", args: [fileName]) @@ -292,11 +291,18 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { } // Update container when at least one file has been added to container - if totalFileCount > failedFileCount { - await refreshContainer(with: container) - let successfulFilesCount = totalFileCount - failedFileCount - successMessage = ToastMessage(key: "Multiple documents added", args: [String(successfulFilesCount)]) - } + guard totalFileCount > failedFileCount else { return } + + await refreshContainer(with: container) + + let successfulFilesCount = totalFileCount - failedFileCount + + successMessage = successfulFilesCount == 1 ? + ToastMessage(key: "Single document added") : + ToastMessage( + key: "Multiple documents added", + args: [String(successfulFilesCount)] + ) } private func refreshContainer(with container: URL) async {