diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4fd2b38..0652a542 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -114,7 +114,7 @@ jobs: -project RIADigiDoc.xcodeproj \ -scheme AllTests \ -destination 'platform=iOS Simulator,name=iPhone 17' \ - -quiet + | xcpretty --test - name: Increment build number run: | agvtool new-version -all "${{ env.BUILD_NUMBER }}" diff --git a/Modules/CommonsLib/Sources/CommonsLib/Constants.swift b/Modules/CommonsLib/Sources/CommonsLib/Constants.swift index 740f7b09..01f526b9 100644 --- a/Modules/CommonsLib/Sources/CommonsLib/Constants.swift +++ b/Modules/CommonsLib/Sources/CommonsLib/Constants.swift @@ -85,8 +85,7 @@ public struct Constants { } public struct Folder { - public static let SignedContainerFolder = "SignedContainers" - public static let CryptoContainerFolder = "CryptoContainers" + public static let ContainerFolder = "Containers" public static let Temp = "tempfiles" public static let Shared = "shareddownloads" public static let SavedFiles = "savedfiles" diff --git a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift index 89d34db7..27d0dc0f 100644 --- a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift +++ b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift @@ -283,7 +283,7 @@ extension CryptoContainer { let sanitizedName = dataFile.key.sanitized() let destinationPath = try Directories.getCacheDirectory( - subfolders: [Constants.Folder.CryptoContainerFolder, Constants.Folder.Temp], + subfolders: [Constants.Folder.ContainerFolder, Constants.Folder.Temp], fileManager: fileManager ) @@ -371,8 +371,35 @@ extension CryptoContainer { dataFiles.append(fileUrl) } + let fileManager = Container.shared.fileManager() + + let cryptoContainersDirectory = try Directories.getCacheDirectory( + subfolders: [Constants.Folder.ContainerFolder], + fileManager: fileManager + ) + + let isFileInTempSignedContainerDirectory = containerFile.absoluteString.hasPrefix( + cryptoContainersDirectory.appending(path: Constants.Folder.Temp, directoryHint: .isDirectory).absoluteString + ) + + let isFileInRecentDocuments = containerFile.absoluteString.hasPrefix( + cryptoContainersDirectory.absoluteString + ) && !isFileInTempSignedContainerDirectory + + var renamedContainerFile = containerFile + + if !isFileInRecentDocuments { + renamedContainerFile = Container.shared.containerUtil().getContainerFile( + for: containerFile, + in: isFileInRecentDocuments ? containerFile.deletingLastPathComponent() : + containerFile.deletingLastPathComponent().deletingLastPathComponent() + ) + + try fileManager.moveItem(at: containerFile, to: renamedContainerFile) + } + return try await create( - containerFile: containerFile, + containerFile: renamedContainerFile, dataFiles: dataFiles, recipients: recipients, isDecrypted: false, diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift index 32ff8af4..422c3597 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift @@ -65,7 +65,7 @@ public actor ContainerWrapper: ContainerWrapperProtocol, Loggable { } @MainActor - public func saveDataFile(containerFile: URL, dataFile: DataFileWrapper, to directory: URL?) async throws -> URL { + public func saveDataFile(dataFile: DataFileWrapper, to directory: URL?) async throws -> URL { let savedFilesDirectory = try directory ?? Directories.getCacheDirectory( subfolders: [CommonsLib.Constants.Folder.SavedFiles], fileManager: fileManager @@ -80,8 +80,8 @@ public actor ContainerWrapper: ContainerWrapperProtocol, Loggable { do { try await DigiDocContainerWrapper.container( - containerFile.resolvedPath, - saveDataFile: dataFile.fileId, + containerURL.resolvedPath, + saveDataFile: dataFile.fileName, to: tempSavedFileLocation.resolvedPath ) ContainerWrapper.logger().info("Successfully saved \(sanitizedFilename) to 'Saved Files' directory") diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift index 4786dfab..4dc6b56e 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift @@ -29,7 +29,7 @@ public protocol ContainerWrapperProtocol: Sendable { func create(file: URL, dataFiles: [String]) async throws func open(containerFile: URL, isSivaConfirmed: Bool) async throws -> ContainerWrapper @discardableResult func addDataFiles(containerFile: URL, dataFiles: [URL]) async throws -> ContainerWrapperProtocol - func saveDataFile(containerFile: URL, dataFile: DataFileWrapper, to directory: URL?) async throws -> URL + func saveDataFile(dataFile: DataFileWrapper, to directory: URL?) async throws -> URL @discardableResult func removeSignature(index: Int, containerFile: URL) async throws -> ContainerWrapperProtocol @discardableResult func removeDataFile(index: Int, containerFile: URL) async throws -> ContainerWrapperProtocol func prepareSignature( @@ -42,7 +42,7 @@ public protocol ContainerWrapperProtocol: Sendable { } extension ContainerWrapperProtocol { - func saveDataFile(containerFile: URL, dataFile: DataFileWrapper) async throws -> URL { - try await saveDataFile(containerFile: containerFile, dataFile: dataFile, to: nil) + func saveDataFile(dataFile: DataFileWrapper) async throws -> URL { + try await saveDataFile(dataFile: dataFile, to: nil) } } diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift index e3fc35ef..05b6a93e 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift @@ -198,15 +198,7 @@ public actor SignedContainer: SignedContainerProtocol, Loggable { } public func saveDataFile(dataFile: DataFileWrapper, to directory: URL?) async throws -> URL { - guard let containerFileURL = containerFile else { - throw DigiDocError.containerDataFileSavingFailed( - ErrorDetail( - message: "Unable to save container. No container file found.", - userInfo: ["fileName": containerFile?.lastPathComponent ?? "N/A"] - ) - ) - } - return try await container.saveDataFile(containerFile: containerFileURL, dataFile: dataFile, to: directory) + return try await container.saveDataFile(dataFile: dataFile, to: directory) } public func getNestedTimestampedContainer() async throws -> SignedContainerProtocol? { @@ -361,7 +353,7 @@ extension SignedContainer { let fileManager = Container.shared.fileManager() let signedContainersDirectory = try Directories.getCacheDirectory( - subfolders: [Constants.Folder.SignedContainerFolder], + subfolders: [Constants.Folder.ContainerFolder], fileManager: fileManager ) diff --git a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Container/ContainerWrapperTests.swift b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Container/ContainerWrapperTests.swift index 461dc0c4..6d666fd0 100644 --- a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Container/ContainerWrapperTests.swift +++ b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Container/ContainerWrapperTests.swift @@ -298,7 +298,7 @@ struct ContainerWrapperTests { return } - let savedFileURL = try await containerWrapper.saveDataFile(containerFile: containerFile, dataFile: dataFile) + let savedFileURL = try await containerWrapper.saveDataFile(dataFile: dataFile) #expect(savedFileURL.isValidURL()) #expect(savedFileURL.lastPathComponent == dataFile.fileName) @@ -306,14 +306,6 @@ struct ContainerWrapperTests { @Test func saveDataFile_throwErrorWhenInvalidDataFile() async throws { - let sampleContainer = try await createSampleContainer(dataFileURLs: dataFileURLs) - - guard let containerFile = await sampleContainer.getRawContainerFile() else { return } - - defer { - try? FileManager.default.removeItem(at: containerFile) - } - let dataFile = MockDataFileWrapper.mockDataFileWrapper( fileId: "", fileName: "datafile-\(UUID().uuidString)", @@ -321,11 +313,15 @@ struct ContainerWrapperTests { mediaType: CommonsLib.Constants.Extension.Default) do { - _ = try await containerWrapper.saveDataFile(containerFile: containerFile, dataFile: dataFile) + _ = try await containerWrapper.saveDataFile(dataFile: dataFile) Issue.record("Expected an error") return } catch let error as DigiDocError { - #expect(error.localizedDescription.contains("unable to save data file")) + guard case .containerDataFileSavingFailed = error else { + Issue.record("Unexpected DigiDocError error: \(error)") + return + } + #expect(true) } catch { Issue.record("Unexpected error: \(error)") return diff --git a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift index 9ff7522d..0127ee65 100644 --- a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift +++ b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift @@ -187,7 +187,7 @@ final class SignedContainerTests { mockContainerUtil.getContainerFileHandler = { _, _ in uniqueFileURL } - mockContainerWrapper.saveDataFileHandler = { _, _, _ in uniqueFileURL } + mockContainerWrapper.saveDataFileHandler = { _, _ in uniqueFileURL } mockFileManager.moveItemHandler = { _, _ in do { @@ -274,7 +274,7 @@ final class SignedContainerTests { return uniqueFileURL } - mockContainerWrapper.saveDataFileHandler = { _, _, _ in uniqueFileURL } + mockContainerWrapper.saveDataFileHandler = { _, _ in uniqueFileURL } mockFileManager.moveItemHandler = { _, _ in do { @@ -371,7 +371,7 @@ final class SignedContainerTests { [MockDataFileWrapper.mockDataFileWrapper()] } - mockContainerWrapper.saveDataFileHandler = { _, _, _ in + mockContainerWrapper.saveDataFileHandler = { _, _ in return URL(fileURLWithPath: "/tmp/mockFile.txt") } @@ -451,7 +451,7 @@ final class SignedContainerTests { mockContainerDataFilesDirURL } - mockContainerWrapper.saveDataFileHandler = { _, _, _ in + mockContainerWrapper.saveDataFileHandler = { _, _ in testContainer } @@ -554,7 +554,7 @@ final class SignedContainerTests { mockContainerDataFilesDirURL } - mockContainerWrapper.saveDataFileHandler = { _, _, _ in + mockContainerWrapper.saveDataFileHandler = { _, _ in throw URLError(.fileDoesNotExist) } diff --git a/Modules/UtilsLib/Sources/UtilsLib/Container/ContainerUtil.swift b/Modules/UtilsLib/Sources/UtilsLib/Container/ContainerUtil.swift index 6453e787..819b22f6 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/Container/ContainerUtil.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/Container/ContainerUtil.swift @@ -55,7 +55,7 @@ public struct ContainerUtil: ContainerUtilProtocol, Loggable { public func getSignatureContainersDir() throws -> URL { let signedContainersDirectory = try Directories.getCacheDirectory( - subfolders: [Constants.Folder.SignedContainerFolder], + subfolders: [Constants.Folder.ContainerFolder], fileManager: fileManager ) diff --git a/Modules/UtilsLib/Sources/UtilsLib/File/FileUtil.swift b/Modules/UtilsLib/Sources/UtilsLib/File/FileUtil.swift index 2499f913..1c433b14 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/File/FileUtil.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/File/FileUtil.swift @@ -115,23 +115,30 @@ public struct FileUtil: FileUtilProtocol, Loggable { FileUtil.logger().info("Checking if file is from iCloud") // Check if file is opened from iCloud if isFileFromiCloud(fileURL: resolvedURL) { - if !isFileDownloadedFromiCloud(fileURL: resolvedURL) { - FileUtil.logger().info( - "File '\(resolvedURL.lastPathComponent)' from iCloud is not downloaded. Downloading..." - ) - - let downloadedFileUrl = await downloadFileFromiCloud(fileURL: resolvedURL) - if let fileUrl = downloadedFileUrl { - FileUtil.logger().info("File '\(resolvedURL.lastPathComponent)' downloaded from iCloud") - return fileUrl - } else { - FileUtil.logger().info( - "Unable to download file '\(resolvedURL.lastPathComponent)' from iCloud") - return nil + do { + return try await withCheckedThrowingContinuation { continuation in + let coordinator = NSFileCoordinator() + var error: NSError? + + coordinator.coordinate( + readingItemAt: url, + options: .withoutChanges, + error: &error + ) { coordURL in + continuation.resume(returning: coordURL) + } + + if let error { + continuation.resume(throwing: error) + } } - } else { - FileUtil.logger().info("File '\(resolvedURL.lastPathComponent)' from iCloud is already downloaded") - return url + } catch { + let fileName = resolvedURL.lastPathComponent + let errorDescription = String(reflecting: error) + FileUtil.logger().error( + "Unable to download file '\(fileName, privacy: .public)' from iCloud. \(errorDescription)" + ) + return nil } } } @@ -208,17 +215,18 @@ public struct FileUtil: FileUtilProtocol, Loggable { public func downloadFileFromiCloud(fileURL: URL) async -> URL? { do { try fileManager.startDownloadingUbiquitousItem(at: fileURL) - FileUtil.logger().info("Downloading file '\(fileURL.lastPathComponent)' from iCloud") + FileUtil.logger().info("Downloading file '\(fileURL.lastPathComponent, privacy: .public)' from iCloud") while !isFileDownloadedFromiCloud(fileURL: fileURL) { try await Task.sleep(for: .seconds(0.5)) } - FileUtil.logger().info("iCloud file '\(fileURL.lastPathComponent)' downloaded") + FileUtil.logger().info("iCloud file '\(fileURL.lastPathComponent, privacy: .public)' downloaded") return fileURL } catch { + let fileName = fileURL.lastPathComponent FileUtil.logger().error( - "Unable to start iCloud file '\(fileURL.lastPathComponent)' download: \(error.localizedDescription)" + "Unable to start iCloud file '\(fileName, privacy: .public)' download: \(error.localizedDescription)" ) return nil } @@ -318,12 +326,14 @@ public struct FileUtil: FileUtilProtocol, Loggable { private func removeDirectory(at url: URL) { let filePath = url.path(percentEncoded: false) - FileUtil.logger().info("Removing \(filePath)") + FileUtil.logger().info("Removing \(filePath, privacy: .public)") do { try fileManager.removeItem(at: url) - FileUtil.logger().info("\(filePath) removed") + FileUtil.logger().info("\(filePath, privacy: .public) removed") } catch { - FileUtil.logger().error("Unable to remove \(filePath): \(error.localizedDescription)") + FileUtil.logger().error( + "Unable to remove \(filePath, privacy: .public): \(error.localizedDescription, privacy: .public)" + ) } } } diff --git a/Modules/UtilsLib/Sources/UtilsLib/System/SystemUtil.swift b/Modules/UtilsLib/Sources/UtilsLib/System/SystemUtil.swift index a162dc08..1df5d998 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/System/SystemUtil.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/System/SystemUtil.swift @@ -32,7 +32,7 @@ public struct SystemUtil: Loggable { public static func getOSVersion() -> String { let osVersion = ProcessInfo.processInfo.operatingSystemVersion let versionString = "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" - logger().info("Operating system version: \(versionString)") + logger().info("Operating system version: \(versionString, privacy: .public)") return versionString } } diff --git a/Modules/UtilsLib/Tests/UtilsLibTests/Container/ContainerUtilTests.swift b/Modules/UtilsLib/Tests/UtilsLibTests/Container/ContainerUtilTests.swift index c78d23be..4d8e7634 100644 --- a/Modules/UtilsLib/Tests/UtilsLibTests/Container/ContainerUtilTests.swift +++ b/Modules/UtilsLib/Tests/UtilsLibTests/Container/ContainerUtilTests.swift @@ -151,7 +151,7 @@ struct ContainerUtilTests { let cachesDir = URL(fileURLWithPath: "/mock/cache") let expectedDir = cachesDir .appending(path: BundleUtil.getBundleIdentifier()) - .appending(path: Constants.Folder.SignedContainerFolder) + .appending(path: Constants.Folder.ContainerFolder) mockFileManager.urlHandler = { directory, _, _, _ in #expect(directory == .cachesDirectory) @@ -184,10 +184,10 @@ struct ContainerUtilTests { @Test func getContainerDataFilesDir_returnDirectoryWhenFileInSignatureDirAndUseCacheDir() throws { let cachesDir = URL(fileURLWithPath: "/mock/cache") - let signatureDir = cachesDir.appending(path: Constants.Folder.SignedContainerFolder) + let signatureDir = cachesDir.appending(path: Constants.Folder.ContainerFolder) let containerFile = signatureDir.appending(path: "file.asice") let expectedDataDir = cachesDir - .appending(path: Constants.Folder.SignedContainerFolder) + .appending(path: Constants.Folder.ContainerFolder) .appending(path: "file.asice-data-files") mockFileManager.urlHandler = { _, _, _, _ in cachesDir } diff --git a/RIADigiDoc.xcodeproj/project.pbxproj b/RIADigiDoc.xcodeproj/project.pbxproj index 309bdf9f..29538743 100644 --- a/RIADigiDoc.xcodeproj/project.pbxproj +++ b/RIADigiDoc.xcodeproj/project.pbxproj @@ -172,6 +172,7 @@ Domain/Model/Error/NFC/DecryptError.swift, Domain/Model/Error/NFC/ReadCertAndSignError.swift, Domain/Model/Error/NFC/UnblockPINError.swift, + Domain/Model/File/FileOpeningMethod.swift, Domain/Model/FileItem.swift, Domain/Model/IdCard/IdCardData.swift, Domain/Model/IdCard/PinResponse.swift, diff --git a/RIADigiDoc/Domain/Model/File/FileOpeningMethod.swift b/RIADigiDoc/Domain/Model/File/FileOpeningMethod.swift new file mode 100644 index 00000000..a4a0207a --- /dev/null +++ b/RIADigiDoc/Domain/Model/File/FileOpeningMethod.swift @@ -0,0 +1,26 @@ +/* + * 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 + +public enum FileOpeningMethod: Int, Sendable { + case all = 0 + case signing = 1 + case crypto = 2 +} diff --git a/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift b/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift index 99e27983..7b3eb2e2 100644 --- a/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift +++ b/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift @@ -83,7 +83,7 @@ actor FileOpeningService: FileOpeningServiceProtocol { from sourceURL: URL, ) async throws -> URL { let signedContainersDirectory = try Directories.getCacheDirectory( - subfolders: [Constants.Folder.SignedContainerFolder], + subfolders: [Constants.Folder.ContainerFolder], fileManager: fileManager ) diff --git a/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift b/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift index 243af1e6..b5e459d2 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift @@ -34,6 +34,7 @@ struct CryptoDataFilesSection: View { @Binding var showSivaMessage: Bool @Binding var isFileSaved: Bool @Binding var showRemoveDataFileModal: Bool + @Binding var navigateToNestedSignedContainerView: Bool init( viewModel: EncryptViewModel, @@ -44,7 +45,8 @@ struct CryptoDataFilesSection: View { selectedDataFile: Binding, showSivaMessage: Binding, isFileSaved: Binding, - showRemoveDataFileModal: Binding + showRemoveDataFileModal: Binding, + navigateToNestedSignedContainerView: Binding ) { self.viewModel = viewModel self.showOpenFileButton = showOpenFileButton @@ -55,6 +57,7 @@ struct CryptoDataFilesSection: View { self._showSivaMessage = showSivaMessage self._isFileSaved = isFileSaved self._showRemoveDataFileModal = showRemoveDataFileModal + self._navigateToNestedSignedContainerView = navigateToNestedSignedContainerView } private var sivaMessage: String { @@ -91,6 +94,10 @@ struct CryptoDataFilesSection: View { } else { await viewModel.handleFileOpening(dataFile: dataFile, isSivaConfirmed: true) } + + await MainActor.run { + navigateToNestedSignedContainerView = viewModel.navigateToNestedSignedContainerView + } } } @@ -105,7 +112,11 @@ struct CryptoDataFilesSection: View { isPresented: $viewModel.isShowingFileSaver, fileURL: viewModel.selectedDataFile, languageSettings: languageSettings, - onComplete: { viewModel.removeSavedFilesDirectory() }, + onComplete: { + if !isNestedContainer { + viewModel.removeSavedFilesDirectory() + } + }, isFileSaved: $isFileSaved ) } @@ -136,3 +147,20 @@ struct CryptoDataFilesSection: View { } } } + +#Preview { + CryptoDataFilesSection( + viewModel: Container.shared.encryptViewModel(), + showOpenFileButton: true, + showSaveFileButton: true, + showRemoveFileButton: true, + isNestedContainer: false, + selectedDataFile: .constant(nil), + showSivaMessage: .constant(false), + isFileSaved: .constant(false), + showRemoveDataFileModal: .constant(false), + navigateToNestedSignedContainerView: .constant(false) + ) + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) +} diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index bbda5265..124b3b1c 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -253,7 +253,9 @@ struct EncryptView: View { fileURL: tempContainerURL, languageSettings: languageSettings, onComplete: { - viewModel.removeSavedFilesDirectory() + if !isNestedContainer { + viewModel.removeSavedFilesDirectory() + } }, isFileSaved: $isFileSaved ) @@ -282,7 +284,9 @@ struct EncryptView: View { selectedDataFile: $selectedDataFile, showSivaMessage: $showSivaMessage, isFileSaved: $isFileSaved, - showRemoveDataFileModal: $showRemoveDataFileModal + showRemoveDataFileModal: $showRemoveDataFileModal, + navigateToNestedSignedContainerView: + $viewModel.navigateToNestedSignedContainerView ) .background( FileSaverHandler( @@ -290,7 +294,9 @@ struct EncryptView: View { fileURL: viewModel.selectedDataFile, languageSettings: languageSettings, onComplete: { - viewModel.removeSavedFilesDirectory() + if !isNestedContainer { + viewModel.removeSavedFilesDirectory() + } }, isFileSaved: $isFileSaved ) @@ -314,7 +320,9 @@ struct EncryptView: View { selectedDataFile: $selectedDataFile, showSivaMessage: $showSivaMessage, isFileSaved: $isFileSaved, - showRemoveDataFileModal: $showRemoveDataFileModal + showRemoveDataFileModal: $showRemoveDataFileModal, + navigateToNestedSignedContainerView: + $viewModel.navigateToNestedSignedContainerView ) } else { CryptoDataFilesLockedSection() @@ -494,6 +502,12 @@ struct EncryptView: View { ) viewModel.resetSuccessMessage() } + .onChange(of: viewModel.navigateToNestedSignedContainerView) { _, isNavigating in + if isNavigating { + viewModel.navigateToNestedSignedContainerView.toggle() + pathManager.navigate(to: .signingView) + } + } } func updateAsyncLabels() async { diff --git a/RIADigiDoc/UI/Component/Container/DataFilesSection.swift b/RIADigiDoc/UI/Component/Container/DataFilesSection.swift index dd86acb0..5ed49e29 100644 --- a/RIADigiDoc/UI/Component/Container/DataFilesSection.swift +++ b/RIADigiDoc/UI/Component/Container/DataFilesSection.swift @@ -32,6 +32,7 @@ struct DataFilesSection: View { @Binding var showSivaMessage: Bool @Binding var isFileSaved: Bool @Binding var showRemoveDataFileModal: Bool + @Binding var navigateToNestedCryptoContainerView: Bool private var sivaMessage: String { languageSettings.localized("Siva message") @@ -48,7 +49,8 @@ struct DataFilesSection: View { selectedDataFile: Binding, showSivaMessage: Binding, isFileSaved: Binding, - showRemoveDataFileModal: Binding + showRemoveDataFileModal: Binding, + navigateToNestedCryptoContainerView: Binding ) { self.viewModel = viewModel self.isContainerSigned = isContainerSigned @@ -57,6 +59,7 @@ struct DataFilesSection: View { self._showSivaMessage = showSivaMessage self._isFileSaved = isFileSaved self._showRemoveDataFileModal = showRemoveDataFileModal + self._navigateToNestedCryptoContainerView = navigateToNestedCryptoContainerView } var body: some View { @@ -84,6 +87,10 @@ struct DataFilesSection: View { } else { await viewModel.handleFileOpening(dataFile: dataFile, isSivaConfirmed: true) } + + await MainActor.run { + navigateToNestedCryptoContainerView = viewModel.navigateToNestedCryptoContainerView + } } } @@ -98,7 +105,11 @@ struct DataFilesSection: View { isPresented: $viewModel.isShowingFileSaver, fileURL: viewModel.selectedDataFile, languageSettings: languageSettings, - onComplete: { viewModel.removeSavedFilesDirectory() }, + onComplete: { + if !isNestedContainer { + viewModel.removeSavedFilesDirectory() + } + }, isFileSaved: $isFileSaved ) } @@ -138,7 +149,8 @@ struct DataFilesSection: View { selectedDataFile: .constant(nil), showSivaMessage: .constant(false), isFileSaved: .constant(false), - showRemoveDataFileModal: .constant(false) + showRemoveDataFileModal: .constant(false), + navigateToNestedCryptoContainerView: .constant(false) ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index 0c289bc3..22012b00 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -58,8 +58,6 @@ struct SigningView: View { @State private var showSivaMessage = false - @State private var isNavigatingToContainerSigningView = false - @AccessibilityFocusState private var focusedField: AccessibilityField? private var containerTitle: String { @@ -231,7 +229,9 @@ struct SigningView: View { fileURL: tempContainerURL, languageSettings: languageSettings, onComplete: { - viewModel.removeSavedFilesDirectory() + if !isNestedContainer { + viewModel.removeSavedFilesDirectory() + } }, isFileSaved: $isFileSaved ) @@ -255,7 +255,9 @@ struct SigningView: View { selectedDataFile: $selectedDataFile, showSivaMessage: $showSivaMessage, isFileSaved: $isFileSaved, - showRemoveDataFileModal: $showRemoveDataFileModal + showRemoveDataFileModal: $showRemoveDataFileModal, + navigateToNestedCryptoContainerView: + $viewModel.navigateToNestedCryptoContainerView ) } else { SignaturesListView( @@ -289,7 +291,9 @@ struct SigningView: View { selectedDataFile: $selectedDataFile, showSivaMessage: $showSivaMessage, isFileSaved: $isFileSaved, - showRemoveDataFileModal: $showRemoveDataFileModal + showRemoveDataFileModal: $showRemoveDataFileModal, + navigateToNestedCryptoContainerView: + $viewModel.navigateToNestedCryptoContainerView ) .background( FileSaverHandler( @@ -297,7 +301,9 @@ struct SigningView: View { fileURL: viewModel.selectedDataFile, languageSettings: languageSettings, onComplete: { - viewModel.removeSavedFilesDirectory() + if !isNestedContainer { + viewModel.removeSavedFilesDirectory() + } }, isFileSaved: $isFileSaved ) @@ -439,6 +445,12 @@ struct SigningView: View { ) viewModel.resetSuccessMessage() } + .onChange(of: viewModel.navigateToNestedCryptoContainerView) { _, isNavigating in + if isNavigating { + viewModel.navigateToNestedCryptoContainerView.toggle() + pathManager.navigate(to: .encryptView(isWithEncryption: false)) + } + } } private func updateSignAndEncryptButtonVisibility() async { diff --git a/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift b/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift index 0b2fd4b0..bed7d924 100644 --- a/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift +++ b/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift @@ -101,7 +101,8 @@ struct CryptoFileOpeningView: View { #Preview { FileOpeningView( isFileOpeningLoading: .constant(true), - isNavigatingToNextView: .constant(false) + isNavigatingToSigningView: .constant(false), + isNavigatingToEncryptView: .constant(false) ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) diff --git a/RIADigiDoc/UI/Component/FileOpeningView.swift b/RIADigiDoc/UI/Component/FileOpeningView.swift index d4f1995c..9b3dd89f 100644 --- a/RIADigiDoc/UI/Component/FileOpeningView.swift +++ b/RIADigiDoc/UI/Component/FileOpeningView.swift @@ -29,7 +29,8 @@ struct FileOpeningView: View { @State private var viewModel: FileOpeningViewModel @Binding var isFileOpeningLoading: Bool - @Binding var isNavigatingToNextView: Bool + @Binding var isNavigatingToSigningView: Bool + @Binding var isNavigatingToEncryptView: Bool @State private var showSivaMessage = false @@ -41,15 +42,22 @@ struct FileOpeningView: View { languageSettings.localized("Siva message url") } + private var isErrorShown: Bool { + guard let errorMessage = viewModel.errorMessage else { return false } + return !errorMessage.key.isEmpty + } + @State private var fileHandlingTask: Task? init( isFileOpeningLoading: Binding, - isNavigatingToNextView: Binding + isNavigatingToSigningView: Binding, + isNavigatingToEncryptView: Binding ) { _viewModel = State(wrappedValue: Container.shared.fileOpeningViewModel()) _isFileOpeningLoading = isFileOpeningLoading - _isNavigatingToNextView = isNavigatingToNextView + _isNavigatingToSigningView = isNavigatingToSigningView + _isNavigatingToEncryptView = isNavigatingToEncryptView } var body: some View { @@ -91,6 +99,11 @@ struct FileOpeningView: View { await viewModel.handleFiles() + if isErrorShown { + await handleFileOpening() + return + } + if await viewModel.isSivaConfirmationNeeded() { showSivaMessage = true } else { @@ -104,7 +117,8 @@ struct FileOpeningView: View { let errorMessage = viewModel.errorMessage if errorMessage == nil { isFileOpeningLoading = viewModel.isFileOpeningLoading - isNavigatingToNextView = viewModel.isNavigatingToNextView + isNavigatingToSigningView = viewModel.isNavigatingToSigningView + isNavigatingToEncryptView = viewModel.isNavigatingToEncryptView let isSivaConfirmed = viewModel.isSivaConfirmed let showFileAddedMessage = await viewModel.showFileAddedMessage() @@ -120,7 +134,8 @@ struct FileOpeningView: View { Toast.show(languageSettings.localized(errorMessage?.key ?? "General error", errorMessage?.args ?? [])) viewModel.handleError() isFileOpeningLoading = viewModel.isFileOpeningLoading - isNavigatingToNextView = viewModel.isNavigatingToNextView + isNavigatingToSigningView = viewModel.isNavigatingToSigningView + isNavigatingToEncryptView = viewModel.isNavigatingToEncryptView } } } @@ -128,7 +143,8 @@ struct FileOpeningView: View { #Preview { FileOpeningView( isFileOpeningLoading: .constant(true), - isNavigatingToNextView: .constant(false) + isNavigatingToSigningView: .constant(false), + isNavigatingToEncryptView: .constant(false), ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) diff --git a/RIADigiDoc/UI/Component/HomeView.swift b/RIADigiDoc/UI/Component/HomeView.swift index dbd7c9d6..ed299810 100644 --- a/RIADigiDoc/UI/Component/HomeView.swift +++ b/RIADigiDoc/UI/Component/HomeView.swift @@ -55,7 +55,26 @@ struct HomeView: View { @State private var sharedFilesLoadingTask: Task? @AccessibilityFocusState private var isFilesButtonFocused: Bool - private var filesBottomSheetActions: [BottomSheetButton] { + private var allContainerFilesBottomSheetActions: [BottomSheetButton] { + HomeViewBottomSheetActions.actions( + onOpenFilesClick: { + isImporting = true + }, + onRecentDocumentsClick: { + containerType = .none + recentDocumentsExtensions = + Constants.Container.ContainerExtensions + Constants.Container.CryptoContainerExtensions + pathManager.navigate(to: + .recentDocumentsView( + folderURL: getRecentDocumentsFolder(containerType: containerType), + extensions: recentDocumentsExtensions + ) + ) + } + ) + } + + private var signedFilesBottomSheetActions: [BottomSheetButton] { HomeViewBottomSheetActions.actions( onOpenFilesClick: { isImporting = true @@ -140,12 +159,14 @@ struct HomeView: View { description: languageSettings.localized("Main home open document description"), assetImageName: "ic_m3_attach_file_48pt_wght400", isFileOpeningLoading: $isFileOpeningLoading, - isNavigatingToNextView: $isNavigatingToSigningView, + isNavigatingToSigningView: $isNavigatingToSigningView, + isNavigatingToEncryptView: $isNavigatingToEncryptView, showBottomSheet: $showFilesBottomSheet, isImporting: $isImporting, - viewModel: viewModel + viewModel: viewModel, + fileOpeningMethod: .all ) - .bottomSheet(isPresented: $showFilesBottomSheet, actions: filesBottomSheetActions) + .bottomSheet(isPresented: $showFilesBottomSheet, actions: allContainerFilesBottomSheetActions) .accessibilityFocused($isFilesButtonFocused) SigningImportButton( @@ -153,12 +174,14 @@ struct HomeView: View { description: languageSettings.localized("Main home signature description"), assetImageName: "ic_m3_stylus_note_48pt_wght400", isFileOpeningLoading: $isFileOpeningLoading, - isNavigatingToNextView: $isNavigatingToSigningView, + isNavigatingToSigningView: $isNavigatingToSigningView, + isNavigatingToEncryptView: $isNavigatingToEncryptView, showBottomSheet: $showSignatureBottomSheet, isImporting: $isImporting, - viewModel: viewModel + viewModel: viewModel, + fileOpeningMethod: .signing ) - .bottomSheet(isPresented: $showSignatureBottomSheet, actions: filesBottomSheetActions) + .bottomSheet(isPresented: $showSignatureBottomSheet, actions: signedFilesBottomSheetActions) CryptoImportButton( title: languageSettings.localized("Main home crypto title"), @@ -168,7 +191,8 @@ struct HomeView: View { isNavigatingToNextView: $isNavigatingToEncryptView, showBottomSheet: $showCryptoBottomSheet, isImporting: $isCryptoImporting, - viewModel: cryptoViewModel + viewModel: cryptoViewModel, + fileOpeningMethod: .crypto ) .bottomSheet(isPresented: $showCryptoBottomSheet, actions: cryptoFilesBottomSheetActions) diff --git a/RIADigiDoc/UI/Component/HomeView/CryptoImportButton.swift b/RIADigiDoc/UI/Component/HomeView/CryptoImportButton.swift index 5cb4486f..67ff8a9d 100644 --- a/RIADigiDoc/UI/Component/HomeView/CryptoImportButton.swift +++ b/RIADigiDoc/UI/Component/HomeView/CryptoImportButton.swift @@ -28,6 +28,7 @@ struct CryptoImportButton: View { @Binding var showBottomSheet: Bool @Binding var isImporting: Bool + var fileOpeningMethod: FileOpeningMethod @State private var viewModel: CryptoHomeViewModel @@ -39,7 +40,8 @@ struct CryptoImportButton: View { isNavigatingToNextView: Binding, showBottomSheet: Binding, isImporting: Binding, - viewModel: CryptoHomeViewModel + viewModel: CryptoHomeViewModel, + fileOpeningMethod: FileOpeningMethod ) { self.title = title self.description = description @@ -49,6 +51,7 @@ struct CryptoImportButton: View { self._showBottomSheet = showBottomSheet self._isImporting = isImporting self.viewModel = viewModel + self.fileOpeningMethod = fileOpeningMethod } var body: some View { @@ -73,6 +76,7 @@ struct CryptoImportButton: View { isFileOpeningLoading = true isImporting = false viewModel.setChosenFiles(result) + viewModel.setFileOpeningMethod(fileOpeningMethod) for url in urls { url.stopAccessingSecurityScopedResource() diff --git a/RIADigiDoc/UI/Component/HomeView/SigningImportButton.swift b/RIADigiDoc/UI/Component/HomeView/SigningImportButton.swift index d528ddcf..7f3675ef 100644 --- a/RIADigiDoc/UI/Component/HomeView/SigningImportButton.swift +++ b/RIADigiDoc/UI/Component/HomeView/SigningImportButton.swift @@ -25,10 +25,12 @@ struct SigningImportButton: View { let description: String let assetImageName: String @Binding var isFileOpeningLoading: Bool - @Binding var isNavigatingToNextView: Bool + @Binding var isNavigatingToSigningView: Bool + @Binding var isNavigatingToEncryptView: Bool @Binding var showBottomSheet: Bool @Binding var isImporting: Bool + var fileOpeningMethod: FileOpeningMethod @State private var viewModel: HomeViewModel @@ -37,19 +39,23 @@ struct SigningImportButton: View { description: String, assetImageName: String, isFileOpeningLoading: Binding, - isNavigatingToNextView: Binding, + isNavigatingToSigningView: Binding, + isNavigatingToEncryptView: Binding, showBottomSheet: Binding, isImporting: Binding, - viewModel: HomeViewModel + viewModel: HomeViewModel, + fileOpeningMethod: FileOpeningMethod ) { self.title = title self.description = description self.assetImageName = assetImageName self._isFileOpeningLoading = isFileOpeningLoading - self._isNavigatingToNextView = isNavigatingToNextView + self._isNavigatingToSigningView = isNavigatingToSigningView + self._isNavigatingToEncryptView = isNavigatingToEncryptView self._showBottomSheet = showBottomSheet self._isImporting = isImporting self.viewModel = viewModel + self.fileOpeningMethod = fileOpeningMethod } var body: some View { @@ -74,6 +80,7 @@ struct SigningImportButton: View { isFileOpeningLoading = true isImporting = false viewModel.setChosenFiles(result) + viewModel.setFileOpeningMethod(fileOpeningMethod) for url in urls { url.stopAccessingSecurityScopedResource() @@ -86,7 +93,8 @@ struct SigningImportButton: View { .fullScreenCover(isPresented: $isFileOpeningLoading) { FileOpeningView( isFileOpeningLoading: $isFileOpeningLoading, - isNavigatingToNextView: $isNavigatingToNextView + isNavigatingToSigningView: $isNavigatingToSigningView, + isNavigatingToEncryptView: $isNavigatingToEncryptView ) } } diff --git a/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift b/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift index edcdb897..4251b899 100644 --- a/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift +++ b/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift @@ -33,6 +33,7 @@ struct RecentDocumentsView: View { @State private var isFileOpeningLoading = false @State private var isNavigatingToSigningView = false + @State private var isNavigatingToEncryptView = false @State private var selectedFile: FileItem? @State private var showRemoveContainerModal = false @@ -150,7 +151,8 @@ struct RecentDocumentsView: View { .fullScreenCover(isPresented: $isFileOpeningLoading) { FileOpeningView( isFileOpeningLoading: $isFileOpeningLoading, - isNavigatingToNextView: $isNavigatingToSigningView + isNavigatingToSigningView: $isNavigatingToSigningView, + isNavigatingToEncryptView: $isNavigatingToEncryptView ) } } diff --git a/RIADigiDoc/ViewModel/CryptoHomeViewModel.swift b/RIADigiDoc/ViewModel/CryptoHomeViewModel.swift index 9d3f4ab7..a86b9ca0 100644 --- a/RIADigiDoc/ViewModel/CryptoHomeViewModel.swift +++ b/RIADigiDoc/ViewModel/CryptoHomeViewModel.swift @@ -57,10 +57,14 @@ class CryptoHomeViewModel: CryptoHomeViewModelProtocol, Loggable { sharedContainerViewModel.setFileOpeningResult(fileOpeningResult: chosenFiles) } + func setFileOpeningMethod(_ method: FileOpeningMethod) { + sharedContainerViewModel.setFileOpeningMethod(method) + } + func getRecentDocumentsFolder() -> URL? { do { return try Directories.getCacheDirectory(fileManager: fileManager) - .appending(path: Constants.Folder.CryptoContainerFolder) + .appending(path: Constants.Folder.ContainerFolder) } catch { CryptoHomeViewModel.logger().error("Unable to get crypto recent documents folder: \(error)") return nil diff --git a/RIADigiDoc/ViewModel/EncryptViewModel.swift b/RIADigiDoc/ViewModel/EncryptViewModel.swift index e8f8e3ef..cee59dc8 100644 --- a/RIADigiDoc/ViewModel/EncryptViewModel.swift +++ b/RIADigiDoc/ViewModel/EncryptViewModel.swift @@ -40,6 +40,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { var isShowingFileSaver = false var showRecipientRemoveButton = false var isLastDataFileRemoved = false + var navigateToNestedSignedContainerView = false private(set) var errorMessage: ToastMessage? private(set) var successMessage: ToastMessage? @@ -86,7 +87,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { } func loadContainerData(cryptoContainer: CryptoContainerProtocol?) async { - EncryptViewModel.logger().info("Loading container data") + EncryptViewModel.logger().info("Loading crypto container data") let openedContainer = (cryptoContainer ?? sharedContainerViewModel.currentContainer()) as? any CryptoContainerProtocol guard let openedContainer else { @@ -102,18 +103,18 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { self.containerMimetype = await openedContainer.getContainerMimetype() self.containerURL = await openedContainer.getRawContainerFile() - EncryptViewModel.logger().info("Container data loaded") + EncryptViewModel.logger().info("Crypto container data loaded") } func createCopyOfContainerForSaving(containerURL: URL?) -> URL? { guard let containerLocation = containerURL else { - EncryptViewModel.logger().error("Unable to get container to create copy for saving") + EncryptViewModel.logger().error("Unable to get crypto container to create copy for saving") return nil } do { let savedFilesDirectory = try Directories.getCacheDirectory( - subfolders: [CommonsLib.Constants.Folder.SavedFiles], + subfolders: [Constants.Folder.SavedFiles, Constants.Folder.Temp], fileManager: fileManager ) @@ -330,14 +331,32 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { return } - if Constants.Extension.CryptoContainers.contains(fileURL.pathExtension) { + let isContainer = await fileURL.isContainer() + let isCryptoContainer = await fileURL.isCryptoContainer() + + if isCryptoContainer { do { + await MainActor.run { + navigateToNestedSignedContainerView = false + } try await openNestedContainer(fileURL: fileURL) - // TODO: Open signed container files } catch { EncryptViewModel.logger().error("Failed to open nested container: \(error)") errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.lastPathComponent]) } + } else if isContainer { + do { + try await openNestedSignedContainer(fileUrl: fileURL) + await MainActor.run { + navigateToNestedSignedContainerView = true + } + } catch { + SigningViewModel.logger().error( + "Failed to open nested signed container: \(String(reflecting: error))" + ) + errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.lastPathComponent]) + return + } } else { previewFile = fileURL } @@ -407,6 +426,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { sharedContainerViewModel.removeLastContainer() let currentContainer = sharedContainerViewModel.currentContainer() as? any CryptoContainerProtocol sharedContainerViewModel.setCryptoContainer(currentContainer) + if currentContainer == nil { return true } await loadContainerData(cryptoContainer: currentContainer) return false } else { @@ -648,12 +668,30 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { } private func openNestedContainer(fileURL: URL) async throws { - if Constants.Extension.CryptoContainers.contains(fileURL.pathExtension) { - let container = try await fileOpeningService - .openOrCreateCryptoContainer(dataFiles: [fileURL]) - await loadContainerData(cryptoContainer: container) - } else { - // TODO: Load nested crypto containers - } + let container = try await fileOpeningService + .openOrCreateCryptoContainer(dataFiles: [fileURL]) + sharedContainerViewModel.setCryptoContainer(container) + await loadContainerData(cryptoContainer: container) + } + + private func openNestedSignedContainer(fileUrl: URL) async throws { + let container = try await ContainerWrapper( + fileManager: fileManager + ).open( + containerFile: fileUrl, + isSivaConfirmed: sivaRepository.isSivaConfirmationNeeded( + files: [fileUrl] + ) + ) + + let signedContainer = SignedContainer( + containerFile: fileUrl, + isExistingContainer: true, + container: container, + fileManager: Container.shared.fileManager(), + containerUtil: Container.shared.containerUtil() + ) + + sharedContainerViewModel.setSignedContainer(signedContainer) } } diff --git a/RIADigiDoc/ViewModel/FileOpeningViewModel.swift b/RIADigiDoc/ViewModel/FileOpeningViewModel.swift index ecad40f0..dc9d79ae 100644 --- a/RIADigiDoc/ViewModel/FileOpeningViewModel.swift +++ b/RIADigiDoc/ViewModel/FileOpeningViewModel.swift @@ -22,12 +22,14 @@ import FactoryKit import LibdigidocLibSwift import CommonsLib import UtilsLib +import CryptoSwift @Observable @MainActor class FileOpeningViewModel: FileOpeningViewModelProtocol, Loggable { var isFileOpeningLoading: Bool = false - var isNavigatingToNextView: Bool = false + var isNavigatingToSigningView: Bool = false + var isNavigatingToEncryptView: Bool = false var isSivaConfirmed = false var signedContainer: SignedContainerProtocol = SignedContainer( @@ -86,14 +88,20 @@ class FileOpeningViewModel: FileOpeningViewModelProtocol, Loggable { sharedContainerViewModel.setAddedFilesCount(addedFiles: files.count) do { - let container = try await fileOpeningRepository.openOrCreateContainer(urls: files, isSivaConfirmed: true) - if await container.getContainerMimetype() == Constants.MimeType.Asics { - try await handleAsicsSivaConfirmation(parentContainer: container) - } else { - sharedContainerViewModel.setSignedContainer(container) + let container = try await openOrCreateContainer(withUrls: files) + if let signedContainer = container as? SignedContainerProtocol { + if await signedContainer.getContainerMimetype() == Constants.MimeType.Asics { + try await handleAsicsSivaConfirmation(parentContainer: signedContainer) + } else { + sharedContainerViewModel.setSignedContainer(signedContainer) + } + + try await signedContainer.getRawContainerFile()?.markAsOpened() + } else if let cryptoContainer = container as? CryptoContainerProtocol { + sharedContainerViewModel.setCryptoContainer(cryptoContainer) + try await cryptoContainer.getRawContainerFile()?.markAsOpened() } - try await container.getRawContainerFile()?.markAsOpened() handleLoadingSuccess(isSivaConfirmed: true) } catch { FileOpeningViewModel.logger().error("Unable to handle SiVa container. \(error)") @@ -140,7 +148,8 @@ class FileOpeningViewModel: FileOpeningViewModelProtocol, Loggable { func handleError() { errorMessage = nil isFileOpeningLoading = false - isNavigatingToNextView = false + isNavigatingToSigningView = false + isNavigatingToEncryptView = false } func isSivaConfirmationNeeded() async -> Bool { @@ -161,9 +170,13 @@ class FileOpeningViewModel: FileOpeningViewModelProtocol, Loggable { } private func handleLoadingSuccess(isSivaConfirmed: Bool) { + let currentContainer = sharedContainerViewModel.currentContainer() + let isSignedContainer = (currentContainer as? SignedContainerProtocol) != nil + let isCryptoContainer = (currentContainer as? CryptoContainerProtocol) != nil self.isSivaConfirmed = isSivaConfirmed isFileOpeningLoading = false - isNavigatingToNextView = true + isNavigatingToSigningView = isSignedContainer + isNavigatingToEncryptView = isCryptoContainer } private func handleError(_ error: Error) { @@ -173,11 +186,47 @@ class FileOpeningViewModel: FileOpeningViewModelProtocol, Loggable { if let dde = error as? DigiDocError { FileOpeningViewModel.logger().error("\(dde)") errorMessage = createToastMessage(for: dde) + let fileName = dde.errorDetail.userInfo["fileName"] as? String + guard let file = fileName else { return } + removeUnsuccessfulContainer(fileName: file) } else { errorMessage = ToastMessage(key: error.localizedDescription) } } + private func removeUnsuccessfulContainer(fileName: String) { + do { + let containerUrl = try Directories.getCacheDirectory( + subfolders: [Constants.Folder.ContainerFolder], + fileManager: fileManager + ) + .appending(path: fileName, directoryHint: .notDirectory) + + try fileManager.removeItem(at: containerUrl) + } catch { + FileOpeningViewModel.logger().error( + "Unable to remove unsuccessful container: \(String(reflecting: error))" + ) + } + } + + private func openOrCreateContainer(withUrls urls: [URL]) async throws -> GeneralContainer { + let fileOpeningMethod = sharedContainerViewModel.getFileOpeningMethod() + switch fileOpeningMethod { + case .all: + guard let firstFile = urls.first else { throw FileOpeningError.noDataFiles } + let isCryptoContainer = await firstFile.isCryptoContainer() + if isCryptoContainer { + return try await fileOpeningRepository.openOrCreateCryptoContainer(urls: urls) + } + return try await fileOpeningRepository.openOrCreateContainer(urls: files, isSivaConfirmed: true) + case .signing: + return try await fileOpeningRepository.openOrCreateContainer(urls: files, isSivaConfirmed: true) + case .crypto: + return try await fileOpeningRepository.openOrCreateCryptoContainer(urls: urls) + } + } + private func createToastMessage(for error: DigiDocError) -> ToastMessage { switch error { case .containerCreationFailed(let errorDetail), diff --git a/RIADigiDoc/ViewModel/HomeViewModel.swift b/RIADigiDoc/ViewModel/HomeViewModel.swift index 60f1025d..e6cfa8d8 100644 --- a/RIADigiDoc/ViewModel/HomeViewModel.swift +++ b/RIADigiDoc/ViewModel/HomeViewModel.swift @@ -59,10 +59,14 @@ class HomeViewModel: HomeViewModelProtocol, Loggable { sharedContainerViewModel.setFileOpeningResult(fileOpeningResult: chosenFiles) } + func setFileOpeningMethod(_ method: FileOpeningMethod) { + sharedContainerViewModel.setFileOpeningMethod(method) + } + func getRecentDocumentsFolder() -> URL? { do { return try Directories.getCacheDirectory(fileManager: fileManager) - .appending(path: Constants.Folder.SignedContainerFolder) + .appending(path: Constants.Folder.ContainerFolder) } catch { HomeViewModel.logger().error("Unable to get signed containers recent documents folder: \(error)") return nil diff --git a/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift index bd716e9a..234a09b5 100644 --- a/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift @@ -27,4 +27,5 @@ public protocol HomeViewModelProtocol: Sendable { func setChosenFiles(_ chosenFiles: Result<[URL], Error>) func getRecentDocumentsFolder() -> URL? func getSharedFiles() async -> [URL] + func setFileOpeningMethod(_ method: FileOpeningMethod) } diff --git a/RIADigiDoc/ViewModel/Protocols/Shared/SharedContainerViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/Shared/SharedContainerViewModelProtocol.swift index 6868db9c..bfea6fdc 100644 --- a/RIADigiDoc/ViewModel/Protocols/Shared/SharedContainerViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/Shared/SharedContainerViewModelProtocol.swift @@ -40,4 +40,7 @@ public protocol SharedContainerViewModelProtocol: Sendable { func setIsSignatureAdded(_ isAdded: Bool) func getIsSignatureAdded() -> Bool + + func setFileOpeningMethod(_ method: FileOpeningMethod) + func getFileOpeningMethod() -> FileOpeningMethod } diff --git a/RIADigiDoc/ViewModel/Shared/SharedContainerViewModel.swift b/RIADigiDoc/ViewModel/Shared/SharedContainerViewModel.swift index 73d56d2c..d662eba5 100644 --- a/RIADigiDoc/ViewModel/Shared/SharedContainerViewModel.swift +++ b/RIADigiDoc/ViewModel/Shared/SharedContainerViewModel.swift @@ -31,6 +31,7 @@ class SharedContainerViewModel: SharedContainerViewModelProtocol { private var addedFilesCount: Int = 0 private var nestedContainers: [GeneralContainer] = [] private var isSignatureAdded: Bool = false + private var fileOpeningMethod: FileOpeningMethod = .all func setSignedContainer(_ signedContainer: SignedContainerProtocol?) { self.signedContainer = signedContainer @@ -58,6 +59,14 @@ class SharedContainerViewModel: SharedContainerViewModelProtocol { return addedFilesCount } + func setFileOpeningMethod(_ method: FileOpeningMethod) { + self.fileOpeningMethod = method + } + + func getFileOpeningMethod() -> FileOpeningMethod { + return fileOpeningMethod + } + private func addNestedContainer(_ container: GeneralContainer?) { guard let container else { return } if !nestedContainers.contains(where: { $0 === container }) { diff --git a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift index 67812691..2e1db8af 100644 --- a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift @@ -487,7 +487,7 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { default: errorMessage = "Signing technical error" - idCardAlertMessageExtraArguments = ["ID card conditional speech"] + errorExtraArguments = ["ID card conditional speech"] } default: diff --git a/RIADigiDoc/ViewModel/SigningViewModel.swift b/RIADigiDoc/ViewModel/SigningViewModel.swift index 5cf1bcb3..24c73bc4 100644 --- a/RIADigiDoc/ViewModel/SigningViewModel.swift +++ b/RIADigiDoc/ViewModel/SigningViewModel.swift @@ -43,6 +43,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { var isCadesContainer = false var isXadesContainer = false var isLastDataFileRemoved = false + var navigateToNestedCryptoContainerView = false private(set) var containerNotifications: [ContainerNotificationType] = [] private(set) var errorMessage: ToastMessage? private(set) var successMessage: ToastMessage? @@ -82,12 +83,12 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { } func loadContainerData(signedContainer: SignedContainerProtocol?) async { - SigningViewModel.logger().info("Loading container data") + SigningViewModel.logger().info("Loading signed container data") sharedContainerViewModel.setIsSignatureAdded(false) let openedContainer = (signedContainer ?? sharedContainerViewModel.currentContainer()) as? any SignedContainerProtocol guard let openedContainer else { - SigningViewModel.logger().error("Cannot load container data. Signed container is nil.") + SigningViewModel.logger().error("Cannot load signed container data. Signed container is nil.") return } @@ -105,7 +106,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { self.containerNotifications = await getContainerNotifications(container: openedContainer) - SigningViewModel.logger().info("Container data loaded") + SigningViewModel.logger().info("Signed container data loaded") } func getContainerNotifications(container: SignedContainerProtocol) async -> [ContainerNotificationType] { @@ -136,13 +137,13 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { func createCopyOfContainerForSaving(containerURL: URL?) -> URL? { guard let containerLocation = containerURL else { - SigningViewModel.logger().error("Unable to get container to create copy for saving") + SigningViewModel.logger().error("Unable to get signed container to create copy for saving") return nil } do { let savedFilesDirectory = try Directories.getCacheDirectory( - subfolders: [CommonsLib.Constants.Folder.SavedFiles], + subfolders: [Constants.Folder.SavedFiles, Constants.Folder.Temp], fileManager: fileManager ) @@ -380,8 +381,14 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { return } - if Constants.MimeType.SignatureContainers.contains(mimeType) { + let isContainer = await fileURL.isContainer() + let isCryptoContainer = await fileURL.isCryptoContainer() + + if isContainer { do { + await MainActor.run { + navigateToNestedCryptoContainerView = false + } try await openNestedContainer(fileURL: fileURL, isSivaConfirmed: isSivaConfirmed) } catch { SigningViewModel.logger().error("Failed to open nested container: \(error)") @@ -396,6 +403,19 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.fileName]) } } + } else if isCryptoContainer { + do { + try await openNestedCryptoContainer(fileUrl: fileURL) + await MainActor.run { + navigateToNestedCryptoContainerView = true + } + } catch { + SigningViewModel.logger().error( + "Failed to open nested crypto container: \(String(reflecting: error))" + ) + errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.fileName]) + return + } } else { previewFile = fileURL } @@ -424,8 +444,10 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { switch result { case .success(let fileURL): return await sivaRepository.isSivaConfirmationNeeded(files: [fileURL]) - case .failure: - errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.fileName]) + case .failure(let error): + SigningViewModel.logger().error( + "Unable to get data file '\(dataFile.fileName)' URL: \(String(reflecting: error))" + ) return false } } @@ -437,10 +459,14 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { } func handleBackButton() async -> Bool { + await MainActor.run { + navigateToNestedCryptoContainerView = false + } if sharedContainerViewModel.containers().count > 1 { sharedContainerViewModel.removeLastContainer() let currentContainer = sharedContainerViewModel.currentContainer() as? any SignedContainerProtocol sharedContainerViewModel.setSignedContainer(currentContainer) + if currentContainer == nil { return true } await loadContainerData(signedContainer: currentContainer) return false } else { @@ -607,4 +633,10 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { await loadContainerData(signedContainer: container) } } + + private func openNestedCryptoContainer(fileUrl: URL) async throws { + let container = try await fileOpeningService + .openOrCreateCryptoContainer(dataFiles: [fileUrl]) + sharedContainerViewModel.setCryptoContainer(container) + } } diff --git a/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift b/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift index 84ea0264..e23cb13b 100644 --- a/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift @@ -119,11 +119,11 @@ struct FileOpeningViewModelTests { await viewModel.handleFiles() let isFileOpeningLoading = viewModel.isFileOpeningLoading - let isNavigatingToNextView = viewModel.isNavigatingToNextView + let isNavigatingToSigningView = viewModel.isNavigatingToSigningView let errorMessage = viewModel.errorMessage?.key #expect(!isFileOpeningLoading) - #expect(!isNavigatingToNextView) + #expect(!isNavigatingToSigningView) #expect(error.localizedDescription == errorMessage) } @@ -143,11 +143,11 @@ struct FileOpeningViewModelTests { await viewModel.handleFiles() let isFileOpeningLoading = viewModel.isFileOpeningLoading - let isNavigatingToNextView = viewModel.isNavigatingToNextView + let isNavigatingToSigningView = viewModel.isNavigatingToSigningView let errorMessage = viewModel.errorMessage?.key #expect(!isFileOpeningLoading) - #expect(!isNavigatingToNextView) + #expect(!isNavigatingToSigningView) #expect(error.localizedDescription == errorMessage) } @@ -177,11 +177,11 @@ struct FileOpeningViewModelTests { await viewModel.handleFiles() let isFileOpeningLoading = viewModel.isFileOpeningLoading - let isNavigatingToNextView = viewModel.isNavigatingToNextView + let isNavigatingToSigningView = viewModel.isNavigatingToSigningView let errorMessage = viewModel.errorMessage?.key #expect(!isFileOpeningLoading) - #expect(!isNavigatingToNextView) + #expect(!isNavigatingToSigningView) #expect(error.localizedDescription == errorMessage) } @@ -195,6 +195,9 @@ struct FileOpeningViewModelTests { return mockContainer } + mockSharedContainerViewModel.getFileOpeningMethodHandler = { .signing } + mockSharedContainerViewModel.currentContainerHandler = { mockContainer } + await viewModel.handleSivaConfirmation() let rawContainerFile = await mockContainer.getRawContainerFile() @@ -211,7 +214,7 @@ struct FileOpeningViewModelTests { ) #expect(viewModel.isSivaConfirmed) - #expect(viewModel.isNavigatingToNextView) + #expect(viewModel.isNavigatingToSigningView) #expect(!viewModel.isFileOpeningLoading) } @@ -224,13 +227,15 @@ struct FileOpeningViewModelTests { mockFileOpeningRepository.openOrCreateContainerHandler = { _, _ in mockMainSignedContainer } mockSivaRepository.isTimestampedContainerHandler = { _ in true } mockSivaRepository.getTimestampedContainerHandler = { _ in mockNestedSignedContainer } + mockSharedContainerViewModel.getFileOpeningMethodHandler = { .signing } + mockSharedContainerViewModel.currentContainerHandler = { mockNestedSignedContainer } await viewModel.handleSivaConfirmation() #expect(mockFileOpeningRepository.openOrCreateContainerCallCount == 1) #expect(mockSharedContainerViewModel.setSignedContainerCallCount == 1) #expect(viewModel.isSivaConfirmed) - #expect(viewModel.isNavigatingToNextView) + #expect(viewModel.isNavigatingToSigningView) } @Test @@ -242,13 +247,15 @@ struct FileOpeningViewModelTests { mockFileOpeningRepository.openOrCreateContainerHandler = { _, _ in mockMainSignedContainer } mockSivaRepository.isTimestampedContainerHandler = { _ in false } mockSivaRepository.getTimestampedContainerHandler = { _ in mockNestedSignedContainer } + mockSharedContainerViewModel.getFileOpeningMethodHandler = { .signing } + mockSharedContainerViewModel.currentContainerHandler = { mockNestedSignedContainer } await viewModel.handleSivaConfirmation() #expect(mockFileOpeningRepository.openOrCreateContainerCallCount == 1) #expect(mockSharedContainerViewModel.setSignedContainerCallCount == 1) #expect(viewModel.isSivaConfirmed) - #expect(viewModel.isNavigatingToNextView) + #expect(viewModel.isNavigatingToSigningView) } @Test @@ -259,10 +266,12 @@ struct FileOpeningViewModelTests { ) } + mockSharedContainerViewModel.getFileOpeningMethodHandler = { .signing } + await viewModel.handleSivaConfirmation() #expect(mockSharedContainerViewModel.setSignedContainerCallCount == 0) - #expect(!viewModel.isNavigatingToNextView) + #expect(!viewModel.isNavigatingToSigningView) } @Test @@ -291,7 +300,7 @@ struct FileOpeningViewModelTests { #expect(mockSharedContainerViewModel.setAddedFilesCountCallCount == 1) #expect(mockSharedContainerViewModel.setSignedContainerCallCount == 0) #expect(!viewModel.isSivaConfirmed) - #expect(!viewModel.isNavigatingToNextView) + #expect(!viewModel.isNavigatingToSigningView) #expect(!viewModel.isFileOpeningLoading) } @@ -321,6 +330,8 @@ struct FileOpeningViewModelTests { return mockSignedContainer } + mockSharedContainerViewModel.currentContainerHandler = { mockSignedContainer } + await viewModel.handleFiles() await viewModel.handleSivaCancellation() @@ -334,7 +345,7 @@ struct FileOpeningViewModelTests { .getRawContainerFile() == rawContainerFile ) #expect(!viewModel.isSivaConfirmed) - #expect(viewModel.isNavigatingToNextView) + #expect(viewModel.isNavigatingToSigningView) #expect(!viewModel.isFileOpeningLoading) } @@ -372,7 +383,7 @@ struct FileOpeningViewModelTests { #expect(mockSharedContainerViewModel.setSignedContainerCallCount == 0) #expect(mockSharedContainerViewModel.setAddedFilesCountCallCount == 1) #expect(!viewModel.isSivaConfirmed) - #expect(!viewModel.isNavigatingToNextView) + #expect(!viewModel.isNavigatingToSigningView) #expect(!viewModel.isFileOpeningLoading) } @@ -402,7 +413,7 @@ struct FileOpeningViewModelTests { #expect(mockSharedContainerViewModel.setSignedContainerCallCount == 0) #expect(mockSharedContainerViewModel.setAddedFilesCountCallCount == 1) #expect(!viewModel.isSivaConfirmed) - #expect(!viewModel.isNavigatingToNextView) + #expect(!viewModel.isNavigatingToSigningView) #expect(!viewModel.isFileOpeningLoading) } @@ -415,7 +426,7 @@ struct FileOpeningViewModelTests { #expect(mockSharedContainerViewModel.setSignedContainerCallCount == 0) #expect(mockSharedContainerViewModel.setAddedFilesCountCallCount == 1) #expect(!viewModel.isSivaConfirmed) - #expect(!viewModel.isNavigatingToNextView) + #expect(!viewModel.isNavigatingToSigningView) #expect(!viewModel.isFileOpeningLoading) } @@ -475,7 +486,7 @@ struct FileOpeningViewModelTests { #expect(viewModel.errorMessage == nil) #expect(!viewModel.isFileOpeningLoading) - #expect(!viewModel.isNavigatingToNextView) + #expect(!viewModel.isNavigatingToSigningView) } @Test diff --git a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift index 21fba8c2..0c8d561f 100644 --- a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift @@ -376,7 +376,7 @@ struct SigningViewModelTests: Loggable { let mockSignedContainer = SignedContainerProtocolMock() let mockNestedSignedContainer = SignedContainerProtocolMock() - let testFile = URL(fileURLWithPath: "/tmp/test.txt") + let testFile = URL(fileURLWithPath: "/tmp/test.asice") let mimeType = CommonsLib.Constants.MimeType.Asice @@ -398,6 +398,8 @@ struct SigningViewModelTests: Loggable { mockFileUtil.fileExistsHandler = { _ in true } + mockSharedContainerViewModel.currentContainerHandler = { mockSignedContainer } + await viewModel.loadContainerData(signedContainer: mockSignedContainer) let currentSignedContainerName = await viewModel.signedContainer?.getContainerName() @@ -462,7 +464,7 @@ struct SigningViewModelTests: Loggable { func handleFileOpening_throwErrorWhenOpeningNestedContainer() async throws { let mockSignedContainer = SignedContainerProtocolMock() - let testFile = URL(fileURLWithPath: "/tmp/test.txt") + let testFile = URL(fileURLWithPath: "/tmp/test.asice") let mimeType = CommonsLib.Constants.MimeType.Asice @@ -487,6 +489,8 @@ struct SigningViewModelTests: Loggable { mockSignedContainer.getContainerNameHandler = { "mockSignedContainer.asice" } + mockSharedContainerViewModel.currentContainerHandler = { mockSignedContainer } + await viewModel.loadContainerData(signedContainer: mockSignedContainer) await viewModel.handleFileOpening(dataFile: testDataFile, isSivaConfirmed: true) diff --git a/codemagic.yaml b/codemagic.yaml index 91243718..56376b82 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -234,6 +234,7 @@ workflows: - *get_google_services_plist - name: "Setup config and TSL files" script: | + export CONFIG_DIRECTORY="$RESOURCES_DIRECTORY/config" export TSL_FILES_DIRECTORY="$CONFIG_DIRECTORY/tslFiles.bundle" # Create TSL folder and mock folders for each module