From 2ddd04b34b173efedf88b1b871ec05a61560b830 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 5 Dec 2025 02:35:14 +0200 Subject: [PATCH 1/8] Fix opening recent documents and placeholders --- .../Sources/CommonsLib/Constants.swift | 4 +- .../Service/CentralConfigurationService.swift | 61 ++++++++---- .../include/Conf/DigiDocConfWrapper.mm | 10 ++ .../include/Signature/DigiDocSigningWrapper.h | 4 +- .../Signature/DigiDocSigningWrapper.mm | 53 +++++++---- .../Domain/Container/ContainerWrapper.swift | 6 +- .../LibdigidocSwift/SignedContainer.swift | 95 ++++++++++++++----- Modules/SmartIdLib/Package.swift | 1 + .../UtilsLib/Container/ContainerUtil.swift | 8 +- .../FileOpening/FileOpeningService.swift | 23 +++-- RIADigiDoc/LibrarySetup.swift | 14 ++- .../Supporting files/Localizable.xcstrings | 58 +++++++---- .../AdvancedSettingsManualCardContent.swift | 1 + .../HomeViewBottomSheetActions.swift | 4 +- .../Signing/MobileId/MobileIdInputView.swift | 7 +- .../Signing/SmartId/SmartIdInputView.swift | 14 ++- .../DiagnosticsView/DiagnosticsSections.swift | 7 +- .../UI/Component/EncryptionSettingsView.swift | 28 +++++- .../InfoView/InfoHeaderHelpButton.swift | 2 +- RIADigiDoc/UI/Component/InitView.swift | 4 +- .../UI/Component/ProxySettingsView.swift | 28 +++++- .../Shared/FloatingLabelTextField.swift | 1 + .../MobileIDSmartIDSettingsView.swift | 7 +- .../ViewModel/CryptoHomeViewModel.swift | 2 +- .../ViewModel/DiagnosticsViewModel.swift | 17 ++-- RIADigiDoc/ViewModel/HomeViewModel.swift | 2 +- .../DiagnosticsViewModelProtocol.swift | 2 +- 27 files changed, 325 insertions(+), 138 deletions(-) diff --git a/Modules/CommonsLib/Sources/CommonsLib/Constants.swift b/Modules/CommonsLib/Sources/CommonsLib/Constants.swift index ab34aead..95990ce5 100644 --- a/Modules/CommonsLib/Sources/CommonsLib/Constants.swift +++ b/Modules/CommonsLib/Sources/CommonsLib/Constants.swift @@ -21,8 +21,6 @@ import Foundation public struct Constants { public struct Container { - public static let SignedContainerFolder = "SignedContainers" - public static let CryptoContainerFolder = "CryptoContainers" public static let DefaultName = "newFile" public static let ContainerExtensions = [ Extension.Asice, @@ -86,6 +84,8 @@ public struct Constants { } public struct Folder { + public static let SignedContainerFolder = "SignedContainers" + public static let CryptoContainerFolder = "CryptoContainers" public static let Temp = "tempfiles" public static let Shared = "shareddownloads" public static let SavedFiles = "savedfiles" diff --git a/Modules/ConfigLib/Sources/ConfigLib/Configuration/Service/CentralConfigurationService.swift b/Modules/ConfigLib/Sources/ConfigLib/Configuration/Service/CentralConfigurationService.swift index 7376c23b..47351302 100644 --- a/Modules/ConfigLib/Sources/ConfigLib/Configuration/Service/CentralConfigurationService.swift +++ b/Modules/ConfigLib/Sources/ConfigLib/Configuration/Service/CentralConfigurationService.swift @@ -18,11 +18,14 @@ */ import Foundation +import OSLog import Alamofire import CommonsLib public actor CentralConfigurationService: CentralConfigurationServiceProtocol { + private static let logger = Logger(subsystem: "ee.ria.digidoc.RIADigiDoc", category: "CentralConfigurationService") + private let userAgent: String private let configurationProperty: ConfigurationProperty private let session: Session? @@ -46,12 +49,19 @@ public actor CentralConfigurationService: CentralConfigurationServiceProtocol { ) let url = "\(await configurationProperty.centralConfigurationServiceUrl)/config.json" - let response: String = try await session.request(url) - .validate() - .serializingString() - .value - return response + do { + let response: String = try await session.request(url) + .validate() + .serializingString() + .value + + return response + } catch { + CentralConfigurationService.logger + .error("Unable to fetch central configuration: \(error)") + throw URLError(.resourceUnavailable) + } } public func fetchPublicKey( @@ -63,12 +73,19 @@ public actor CentralConfigurationService: CentralConfigurationServiceProtocol { ) let url = "\(await configurationProperty.centralConfigurationServiceUrl)/config.pub" - let response: String = try await session.request(url) - .validate() - .serializingString() - .value - return response + do { + let response: String = try await session.request(url) + .validate() + .serializingString() + .value + + return response + } catch { + CentralConfigurationService.logger + .error("Unable to fetch central configuration public key: \(error)") + throw URLError(.resourceUnavailable) + } } public func fetchSignature( @@ -80,16 +97,22 @@ public actor CentralConfigurationService: CentralConfigurationServiceProtocol { ) let url = "\(await configurationProperty.centralConfigurationServiceUrl)/config.rsa" - let responseData: Data = try await session.request(url) - .validate() - .serializingData() - .value - - guard let responseString = String(data: responseData, encoding: .utf8) else { - throw URLError(.cannotDecodeContentData) + do { + let responseData: Data = try await session.request(url) + .validate() + .serializingData() + .value + + guard let responseString = String(data: responseData, encoding: .utf8) else { + throw URLError(.cannotDecodeContentData) + } + + return responseString + } catch { + CentralConfigurationService.logger + .error("Unable to fetch central configuration signature: \(error)") + throw URLError(.resourceUnavailable) } - - return responseString } private func constructHttpClient( diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Conf/DigiDocConfWrapper.mm b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Conf/DigiDocConfWrapper.mm index c0a18d97..b00b0ce3 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Conf/DigiDocConfWrapper.mm +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Conf/DigiDocConfWrapper.mm @@ -87,6 +87,16 @@ void setProxyPass(NSString *proxyPass) { return digidoc::ConfCurrent::TSLUrl(); } + std::vector TSLCerts() const final { + NSMutableArray *certBundle = [NSMutableArray arrayWithArray:currentConf.TSLCERTS]; + + if (certBundle != nil && certBundle.count > 0) { + return toX509Certs(certBundle); + } + + return digidoc::ConfCurrent::TSLCerts(); + } + std::vector TSCerts() const override { NSMutableArray *certBundle = [NSMutableArray arrayWithArray:currentConf.CERTBUNDLE]; diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Signature/DigiDocSigningWrapper.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Signature/DigiDocSigningWrapper.h index 3612d138..f7b5a235 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Signature/DigiDocSigningWrapper.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Signature/DigiDocSigningWrapper.h @@ -24,8 +24,8 @@ NS_ASSUME_NONNULL_BEGIN @interface DigiDocSigningWrapper : NSObject -+ (void)prepareSignature:(NSData *)cert containerPath:(NSString *)containerPath roleData:(DigiDocRoleData *)roleData userAgent:(NSString *)userAgent completion:(void (^)(NSData * _Nullable dataToSign, NSError * _Nullable error))completion; -+ (void)addSignature:(NSData *)data completion:(void (^)(BOOL valid, NSError * _Nullable error))completion; +- (void)prepareSignature:(NSData *)cert containerPath:(NSString *)containerPath roleData:(DigiDocRoleData *)roleData userAgent:(NSString *)userAgent completion:(void (^)(NSData * _Nullable dataToSign, NSError * _Nullable error))completion; +- (void)addSignature:(NSData *)data completion:(void (^)(BOOL valid, NSError * _Nullable error))completion; @end diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Signature/DigiDocSigningWrapper.mm b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Signature/DigiDocSigningWrapper.mm index 2295266e..e5a6b30a 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Signature/DigiDocSigningWrapper.mm +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Signature/DigiDocSigningWrapper.mm @@ -34,30 +34,40 @@ #import "../Model/DigiDocRoleData.h" #import "Exception/Util/ExceptionUtil.h" -@implementation DigiDocSigningWrapper {} +@implementation DigiDocSigningWrapper { + std::unique_ptr _docContainer; + digidoc::Signature *_signature; + std::unique_ptr _signer; +} -static std::unique_ptr docContainer = nil; -static digidoc::Signature *signature = nil; -static std::unique_ptr signer{}; +- (instancetype)init { + self = [super init]; + if (self) { + _docContainer = nil; + _signature = nil; + _signer = {}; + } + return self; +} -+ (void)prepareSignature:(NSData *)cert containerPath:(NSString *)containerPath roleData:(DigiDocRoleData *)roleData userAgent:(NSString *)userAgent completion:(void (^)(NSData * _Nullable, NSError * _Nullable))completion { +- (void)prepareSignature:(NSData *)cert containerPath:(NSString *)containerPath roleData:(DigiDocRoleData *)roleData userAgent:(NSString *)userAgent completion:(void (^)(NSData * _Nullable, NSError * _Nullable))completion { NSError *error = nil; try { - signer = std::make_unique(digidoc::X509Cert(reinterpret_cast(cert.bytes), cert.length)); - signature = NULL; + _signer = std::make_unique(digidoc::X509Cert(reinterpret_cast(cert.bytes), cert.length)); + _signature = NULL; DigiDocContainerOpenCB cb(TRUE); - docContainer = digidoc::Container::openPtr(containerPath.UTF8String, &cb); + _docContainer = digidoc::Container::openPtr(containerPath.UTF8String, &cb); - signer->setProfile("time-stamp"); + _signer->setProfile("time-stamp"); - signer->setSignatureProductionPlace(roleData.city.UTF8String ?: "", + _signer->setSignatureProductionPlace(roleData.city.UTF8String ?: "", roleData.state.UTF8String ?: "", roleData.zipcode.UTF8String ?: "", roleData.country.UTF8String ?: ""); - signer->setUserAgent(userAgent.UTF8String); + _signer->setUserAgent(userAgent.UTF8String); std::vector rolesList; for (NSString *role in roleData.roles) { @@ -65,10 +75,11 @@ + (void)prepareSignature:(NSData *)cert containerPath:(NSString *)containerPath rolesList.push_back(role.UTF8String); } } - signer->setSignerRoles(rolesList); - signature = docContainer->prepareSignature(signer.get()); - NSData *data = [DigiDocSigningWrapper getNSDataFromVector:signature->dataToSign()]; + _signer->setSignerRoles(rolesList); + + _signature = _docContainer->prepareSignature(_signer.get()); + NSData *data = [DigiDocSigningWrapper getNSDataFromVector:_signature->dataToSign()]; if (completion) completion(data, nil); } catch(const digidoc::Exception &e) { std::vector causes = e.causes(); @@ -83,25 +94,25 @@ + (void)prepareSignature:(NSData *)cert containerPath:(NSString *)containerPath } } -+ (void)addSignature:(NSData *)data completion:(void (^)(BOOL success, NSError * _Nullable error))completion { +- (void)addSignature:(NSData *)data completion:(void (^)(BOOL success, NSError * _Nullable error))completion { NSError *error = nil; - if (!signature) { + if (!_signature) { error = [NSError errorWithDomain:@"LibdigidocLib" code:2 userInfo:@{ NSLocalizedDescriptionKey: @"Did not find signature" }]; if (completion) completion(NO, error); } - if (auto timeStampTime = signature->TimeStampTime(); !timeStampTime.empty()) { + if (auto timeStampTime = _signature->TimeStampTime(); !timeStampTime.empty()) { if (completion) completion(YES, error); } try { auto *bytes = reinterpret_cast(data.bytes); - signature->setSignatureValue({bytes, bytes + data.length}); - signature->extendSignatureProfile(signer.get()); - signature->validate(); - docContainer->save(); + _signature->setSignatureValue({bytes, bytes + data.length}); + _signature->extendSignatureProfile(_signer.get()); + _signature->validate(); + _docContainer->save(); if (completion) completion(YES, error); } catch(const digidoc::Exception &e) { std::vector causes = e.causes(); diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift index 764c22cd..64bf491c 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift @@ -33,6 +33,8 @@ public actor ContainerWrapper: ContainerWrapperProtocol { private let fileManager: FileManagerProtocol + private let digiDocSigningWrapper: DigiDocSigningWrapper = DigiDocSigningWrapper() + public init( containerURL: URL = URL(fileURLWithPath: ""), dataFiles: [DataFileWrapper] = [], @@ -202,7 +204,7 @@ public actor ContainerWrapper: ContainerWrapperProtocol { roleData: RoleData?, userAgent: String ) async throws -> Data { - return try await DigiDocSigningWrapper + return try await digiDocSigningWrapper .prepareSignature( cert, containerPath: containerPath.resolvedPath, @@ -220,7 +222,7 @@ public actor ContainerWrapper: ContainerWrapperProtocol { public func addSignature(signature: Data, containerFile: URL) async throws -> ContainerWrapperProtocol { do { - try await DigiDocSigningWrapper.addSignature(signature) + try await digiDocSigningWrapper.addSignature(signature) return try await open(containerFile: containerFile, isSivaConfirmed: true) } catch { diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift index 2e5d6e72..56e4e9b9 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift @@ -331,32 +331,60 @@ extension SignedContainer { guard let containerFile else { let error = isFirstDataFileContainer - ? DigiDocError.containerOpeningFailed( - ErrorDetail( - message: "Cannot open container. Container file is nil")) - : DigiDocError.containerCreationFailed( - ErrorDetail( - message: "Cannot create container. Container file is nil" - ) + ? DigiDocError.containerOpeningFailed( + ErrorDetail( + message: "Cannot open container. Container file is nil")) + : DigiDocError.containerCreationFailed( + ErrorDetail( + message: "Cannot create container. Container file is nil" ) + ) throw error } if dataFiles.count == 1 && isFirstDataFileContainer { SignedContainer.logger.debug("Opening existing container") return try await open(file: containerFile, isSivaConfirmed: isSivaConfirmed) - } else { - SignedContainer.logger.debug("Creating a new container") - return try await create(containerFile: containerFile, dataFiles: dataFiles) } + + SignedContainer.logger.debug("Creating a new container") + return try await create(containerFile: containerFile, dataFiles: dataFiles) } private static func open(file: URL, isSivaConfirmed: Bool) async throws -> SignedContainerProtocol { + let fileManager = Container.shared.fileManager() + + let signedContainersDirectory = try Directories.getCacheDirectory( + subfolder: Constants.Folder.SignedContainerFolder, + fileManager: fileManager + ) + + let isFileInTempSignedContainerDirectory = file.absoluteString.hasPrefix( + signedContainersDirectory.appending(path: Constants.Folder.Temp, directoryHint: .isDirectory).absoluteString + ) + + let isFileInRecentDocuments = file.absoluteString.hasPrefix( + signedContainersDirectory.absoluteString + ) && !isFileInTempSignedContainerDirectory + + var renamedContainerFile = file + + if !isFileInRecentDocuments { + renamedContainerFile = Container.shared.containerUtil().getContainerFile( + for: file, + in: isFileInRecentDocuments ? file.deletingLastPathComponent() : + file.deletingLastPathComponent().deletingLastPathComponent() + ) + + try fileManager.moveItem(at: file, to: renamedContainerFile) + } + let container = try await ContainerWrapper( - fileManager: Container.shared.fileManager() - ).open(containerFile: file, isSivaConfirmed: isSivaConfirmed) + fileManager: fileManager + ).open(containerFile: renamedContainerFile, isSivaConfirmed: isSivaConfirmed) + return SignedContainer( - containerFile: file, + containerFile: renamedContainerFile, isExistingContainer: true, container: container, fileManager: Container.shared.fileManager(), @@ -370,15 +398,42 @@ extension SignedContainer { ) async throws -> SignedContainerProtocol { let fileManager = Container.shared.fileManager() let containerWrapper = ContainerWrapper( - fileManager: Container.shared.fileManager() + fileManager: fileManager ) - try await containerWrapper.create(file: containerFile, dataFiles: dataFiles.compactMap { $0.resolvedPath }) + let renamedContainerFile = Container.shared.containerUtil().getContainerFile( + for: containerFile, + in: containerFile.deletingLastPathComponent().deletingLastPathComponent() + ) + + try await containerWrapper + .create( + file: renamedContainerFile, + dataFiles: dataFiles.compactMap { + $0.resolvedPath + } + ) + + let createdContainer = try await containerWrapper.open( + containerFile: renamedContainerFile, + isSivaConfirmed: true + ) - let createdContainer = try await containerWrapper.open(containerFile: containerFile, isSivaConfirmed: true) + let signedContainer = SignedContainer( + containerFile: renamedContainerFile, + isExistingContainer: false, + container: createdContainer, + fileManager: Container.shared.fileManager(), + containerUtil: Container.shared.containerUtil() + ) SignedContainer.logger.debug("Container created. Removing \(dataFiles.count) saved data files") for (index, dataFile) in dataFiles.enumerated() { + let containerName = await signedContainer.getContainerName() + if await dataFile.isContainer() && containerName == dataFile.lastPathComponent { + continue + } + SignedContainer.logger.debug( "Removing data file (\(index + 1) / \(dataFiles.count)): \(dataFile.lastPathComponent)" ) @@ -389,12 +444,6 @@ extension SignedContainer { } } - return SignedContainer( - containerFile: containerFile, - isExistingContainer: false, - container: createdContainer, - fileManager: Container.shared.fileManager(), - containerUtil: Container.shared.containerUtil() - ) + return signedContainer } } diff --git a/Modules/SmartIdLib/Package.swift b/Modules/SmartIdLib/Package.swift index 872f0526..7d7b31a0 100644 --- a/Modules/SmartIdLib/Package.swift +++ b/Modules/SmartIdLib/Package.swift @@ -45,6 +45,7 @@ let package = Package( dependencies: [ "SmartIdLib", "UtilsLib", + "CommonsLib", .product(name: "UtilsLibMocks", package: "utilslib"), .product(name: "CommonsLibMocks", package: "commonslib"), .product(name: "FactoryTesting", package: "Factory") diff --git a/Modules/UtilsLib/Sources/UtilsLib/Container/ContainerUtil.swift b/Modules/UtilsLib/Sources/UtilsLib/Container/ContainerUtil.swift index 8b907f41..7fa22bf7 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/Container/ContainerUtil.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/Container/ContainerUtil.swift @@ -40,11 +40,11 @@ public struct ContainerUtil: ContainerUtilProtocol { ) -> URL { let fileExtension = fileURL.pathExtension let baseName = fileURL.deletingPathExtension().lastPathComponent - var uniqueFileURL = fileURL + var uniqueFileURL = directory.appending(path: fileURL.lastPathComponent) var fileNameCounter = 1 - while fileManager.fileExists(atPath: uniqueFileURL.resolvedPath) { - let newFileName = "\(baseName)-\(fileNameCounter)" + while fileManager.fileExists(atPath: uniqueFileURL.path) { + let newFileName = "\(baseName) (\(fileNameCounter))" if !fileExtension.isEmpty { uniqueFileURL = directory.appending(path: "\(newFileName).\(fileExtension)") } else { @@ -58,7 +58,7 @@ public struct ContainerUtil: ContainerUtilProtocol { public func getSignatureContainersDir() throws -> URL { let signedContainersDirectory = try Directories.getCacheDirectory( - subfolder: Constants.Container.SignedContainerFolder, + subfolder: Constants.Folder.SignedContainerFolder, fileManager: fileManager ) diff --git a/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift b/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift index 80a3babc..eb701451 100644 --- a/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift +++ b/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift @@ -27,9 +27,7 @@ import CryptoSwift actor FileOpeningService: FileOpeningServiceProtocol { private let fileUtil: FileUtilProtocol - private let fileInspector: FileInspectorProtocol - private let fileManager: FileManagerProtocol init( @@ -54,10 +52,7 @@ actor FileOpeningService: FileOpeningServiceProtocol { var validFiles: [URL] = [] for url in urls { - guard url - .startAccessingSecurityScopedResource() else { - continue - } + _ = url.startAccessingSecurityScopedResource() let validUrl = try await url.validURL(fileUtil: fileUtil) @@ -66,7 +61,7 @@ actor FileOpeningService: FileOpeningServiceProtocol { } if try await isFileSizeValid(url: validUrl) { - validFiles.append(try cacheFile(from: validUrl)) + await validFiles.append(try cacheFile(from: validUrl)) } } @@ -86,9 +81,9 @@ actor FileOpeningService: FileOpeningServiceProtocol { private func cacheFile( from sourceURL: URL, - ) throws -> URL { + ) async throws -> URL { let signedContainersDirectory = try Directories.getCacheDirectory( - subfolder: Constants.Container.SignedContainerFolder, + subfolder: Constants.Folder.SignedContainerFolder, fileManager: fileManager ) @@ -97,14 +92,18 @@ actor FileOpeningService: FileOpeningServiceProtocol { return sourceURL } - if !fileManager.fileExists(atPath: signedContainersDirectory.resolvedPath) { + let signedContainersDataFilesDirectory = signedContainersDirectory + .appending(path: Constants.Folder.Temp, directoryHint: .isDirectory) + + if !fileManager.fileExists(atPath: signedContainersDirectory.resolvedPath) || + !fileManager.fileExists(atPath: signedContainersDataFilesDirectory.resolvedPath) { try fileManager.createDirectory( - at: signedContainersDirectory, + at: signedContainersDataFilesDirectory, withIntermediateDirectories: true, attributes: nil) } - let destinationURL = signedContainersDirectory.appending(path: sourceURL.lastPathComponent) + let destinationURL = signedContainersDataFilesDirectory.appending(path: sourceURL.lastPathComponent) if fileManager.fileExists(atPath: destinationURL.resolvedPath) { try fileManager.removeItem(at: destinationURL) diff --git a/RIADigiDoc/LibrarySetup.swift b/RIADigiDoc/LibrarySetup.swift index 81077c11..05825af8 100644 --- a/RIADigiDoc/LibrarySetup.swift +++ b/RIADigiDoc/LibrarySetup.swift @@ -79,10 +79,16 @@ actor LibrarySetup { ).appending(path: CommonsLib.Constants.Configuration.CacheConfigFolder ) - try await configurationLoader.initConfiguration( - cacheDir: configDirectory, - proxyInfo: proxyInfo - ) + + // Make sure "initDigiDoc" is still run even if configuration has an error + do { + try await configurationLoader.initConfiguration( + cacheDir: configDirectory, + proxyInfo: proxyInfo + ) + } catch { + LibrarySetup.logger.error("Unable to initialize configuration: \(error)") + } LibrarySetup.logger.debug("Initializing Libdigidocpp") try await DigiDocConf.initDigiDoc( diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index 23617203..201bc165 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -1,6 +1,24 @@ { "sourceLanguage" : "en", "strings" : { + "Add file" : { + "comment" : "Home screen bottom sheet action", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add file" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lisa fail" + } + } + } + }, "Add more files" : { "comment" : "Add more files button in signature view and crypto view", "extractionState" : "manual", @@ -1416,23 +1434,6 @@ } } }, - "Help" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Help" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abi" - } - } - } - }, "Hide password" : { "comment" : "Hide password", "extractionState" : "manual", @@ -1649,6 +1650,24 @@ } } }, + "Information system authority" : { + "comment" : "RIA name. Used in initial language screen", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estonian Information System Authority" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riigi Infosüsteemi Amet" + } + } + } + }, "Init lang locale en" : { "comment" : "Init lang locale en", "extractionState" : "manual", @@ -4285,7 +4304,7 @@ "et" : { "stringUnit" : { "state" : "translated", - "value" : "Dokumendid puuduvad" + "value" : "Ümbrikud puuduvad" } } } @@ -5080,9 +5099,6 @@ } } } - }, - "Riigi Infosüsteemide Amet" : { - }, "Role and address title" : { "comment" : "Role and address view title", diff --git a/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsManualCardContent.swift b/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsManualCardContent.swift index 2b90e015..223cc39d 100644 --- a/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsManualCardContent.swift +++ b/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsManualCardContent.swift @@ -45,6 +45,7 @@ struct AdvancedSettingsManualCardContent: View { content: { FloatingLabelTextField( title: textFieldTitle, + placeholder: textFieldTitle, text: $textFieldText, ) AdvancedSettingsCertificateSection( diff --git a/RIADigiDoc/UI/Component/Bottom Sheet/HomeViewBottomSheetActions.swift b/RIADigiDoc/UI/Component/Bottom Sheet/HomeViewBottomSheetActions.swift index 321f1415..d2f2d558 100644 --- a/RIADigiDoc/UI/Component/Bottom Sheet/HomeViewBottomSheetActions.swift +++ b/RIADigiDoc/UI/Component/Bottom Sheet/HomeViewBottomSheetActions.swift @@ -25,8 +25,8 @@ struct HomeViewBottomSheetActions { [ BottomSheetButton( icon: "ic_m3_attach_file_48pt_wght400", - title: "Open file", - accessibilityLabel: "Open file", + title: "Add file", + accessibilityLabel: "Add file", showExtraIcon: true, extraIcon: "ic_m3_arrow_right_48pt_wght400", onClick: onOpenFilesClick, diff --git a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdInputView.swift b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdInputView.swift index 49a06a0a..488bf8fd 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdInputView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdInputView.swift @@ -37,6 +37,10 @@ struct MobileIdInputView: View { let onInputChange: () -> Void + private var personalCodeTitle: String { + languageSettings.localized("Personal code") + } + private var countryCodeAndPhoneErrorText: String { return languageSettings.localized(countryCodeAndPhoneError ?? "") } @@ -87,7 +91,8 @@ struct MobileIdInputView: View { VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { FloatingLabelTextField( - title: languageSettings.localized("Personal code"), + title: personalCodeTitle, + placeholder: personalCodeTitle, text: $personalCode, isError: !personalCodeErrorText.isEmpty, errorText: personalCodeErrorText, diff --git a/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdInputView.swift b/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdInputView.swift index 4218a7b7..ae6bb4f8 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdInputView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdInputView.swift @@ -36,6 +36,14 @@ struct SmartIdInputView: View { let onInputChange: () -> Void + private var countryTitle: String { + languageSettings.localized("Country title") + } + + private var personalCodeTitle: String { + languageSettings.localized("Personal code") + } + private var personalCodeErrorText: String { return languageSettings.localized(personalCodeError ?? "") } @@ -65,7 +73,8 @@ struct SmartIdInputView: View { VStack(alignment: .leading, spacing: Dimensions.Padding.MPadding) { VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { FloatingLabelTextField( - title: languageSettings.localized("Country title"), + title: countryTitle, + placeholder: countryTitle, text: .constant(languageSettings.localized(country.rawValue)), isDropdown: true, isDisabled: false, @@ -77,7 +86,8 @@ struct SmartIdInputView: View { VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { FloatingLabelTextField( - title: languageSettings.localized("Personal code"), + title: personalCodeTitle, + placeholder: personalCodeTitle, text: $personalCode, isError: !personalCodeErrorText.isEmpty, errorText: personalCodeErrorText, diff --git a/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSections.swift b/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSections.swift index 495f2e88..fcf53c16 100644 --- a/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSections.swift +++ b/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSections.swift @@ -27,7 +27,7 @@ struct DiagnosticsSections: View { var versionSectionContent: String var osSectionContent: (key: String, content: String) var libdigidocVersion: String - var urlSectionContent: [String] + var urlSectionContent: [(key: String, content: String)] var cdoc2SectionContent: [String] var tslSectionContent: [String] var centralConfigurationSectionContent: [(key: String, content: String)] @@ -51,7 +51,8 @@ struct DiagnosticsSections: View { DiagnosticsSingleSection( title: languageSettings.localized("Main diagnostics urls title"), - contentLines: urlSectionContent, + contentLines: urlSectionContent + .map { "\($0.key): \(languageSettings.localized($0.content))" }, showDivider: false, ) @@ -81,7 +82,7 @@ struct DiagnosticsSections: View { versionSectionContent: "", osSectionContent: (key: "", content: ""), libdigidocVersion: "", - urlSectionContent: [""], + urlSectionContent: [(key: "", content: "")], cdoc2SectionContent: [""], tslSectionContent: [""], centralConfigurationSectionContent: [(key: "", content: "")] diff --git a/RIADigiDoc/UI/Component/EncryptionSettingsView.swift b/RIADigiDoc/UI/Component/EncryptionSettingsView.swift index b4797d13..4f62d881 100644 --- a/RIADigiDoc/UI/Component/EncryptionSettingsView.swift +++ b/RIADigiDoc/UI/Component/EncryptionSettingsView.swift @@ -42,6 +42,22 @@ struct EncryptionSettingsView: View { @State private var viewModel: EncryptionSettingsViewModel + private var cryptoServerTitle: String { + languageSettings.localized("Main settings crypto server") + } + + private var cryptoUuidTitle: String { + languageSettings.localized("Main settings crypto uuid") + } + + private var cryptoFetchUrlTitle: String { + languageSettings.localized("Main settings crypto fetch url") + } + + private var cryptoPostUrlTitle: String { + languageSettings.localized("Main settings crypto post url") + } + init() { _viewModel = State(wrappedValue: Container.shared.encryptionSettingsViewModel()) } @@ -119,7 +135,8 @@ struct EncryptionSettingsView: View { ) if viewModel.useKeyTransfer { FloatingLabelTextField( - title: languageSettings.localized("Main settings crypto server"), + title: cryptoServerTitle, + placeholder: cryptoServerTitle, text: .constant(languageSettings.localized(selectedServerOption?.titleKey ?? "")), isDropdown: true, isDisabled: !viewModel.useKeyTransfer, @@ -131,19 +148,22 @@ struct EncryptionSettingsView: View { ) FloatingLabelTextField( - title: languageSettings.localized("Main settings crypto uuid"), + title: cryptoUuidTitle, + placeholder: cryptoUuidTitle, text: $viewModel.serverInfo.uuid, isDisabled: !viewModel.useKeyTransfer || viewModel.serverId != viewModel.cdoc2ManualKeyTransferServerUUID ) FloatingLabelTextField( - title: languageSettings.localized("Main settings crypto fetch url"), + title: cryptoFetchUrlTitle, + placeholder: cryptoFetchUrlTitle, text: $viewModel.serverInfo.fetchURL, isDisabled: !viewModel.useKeyTransfer || viewModel.serverId != viewModel.cdoc2ManualKeyTransferServerUUID ) FloatingLabelTextField( - title: languageSettings.localized("Main settings crypto post url"), + title: cryptoPostUrlTitle, + placeholder: cryptoPostUrlTitle, text: $viewModel.serverInfo.postURL, isDisabled: !viewModel.useKeyTransfer || viewModel.serverId != viewModel.cdoc2ManualKeyTransferServerUUID diff --git a/RIADigiDoc/UI/Component/InfoView/InfoHeaderHelpButton.swift b/RIADigiDoc/UI/Component/InfoView/InfoHeaderHelpButton.swift index c8581933..7ceb6fee 100644 --- a/RIADigiDoc/UI/Component/InfoView/InfoHeaderHelpButton.swift +++ b/RIADigiDoc/UI/Component/InfoView/InfoHeaderHelpButton.swift @@ -46,7 +46,7 @@ struct InfoHeaderHelpButton: View { .frame(width: Dimensions.Icon.IconSizeXXS) .foregroundStyle(theme.onPrimary) .accessibilityHidden(true) - Text(helpLabel) + Text(verbatim: helpLabel) .font(typography.labelMedium) .foregroundStyle(theme.onPrimary) diff --git a/RIADigiDoc/UI/Component/InitView.swift b/RIADigiDoc/UI/Component/InitView.swift index badce599..60995e2f 100644 --- a/RIADigiDoc/UI/Component/InitView.swift +++ b/RIADigiDoc/UI/Component/InitView.swift @@ -84,7 +84,9 @@ struct InitView: View { .font(typography.titleMedium) .foregroundStyle(Color.white) .padding(.bottom, Dimensions.Padding.MPadding) - .accessibilityLabel("Riigi Infosüsteemide Amet") + .accessibilityLabel( + Text(verbatim: languageSettings.localized("Information system authority")) + ) } .frame(minHeight: geometry.size.height) .frame(minWidth: geometry.size.width) diff --git a/RIADigiDoc/UI/Component/ProxySettingsView.swift b/RIADigiDoc/UI/Component/ProxySettingsView.swift index 258ccabb..cc9375ad 100644 --- a/RIADigiDoc/UI/Component/ProxySettingsView.swift +++ b/RIADigiDoc/UI/Component/ProxySettingsView.swift @@ -28,6 +28,22 @@ struct ProxySettingsView: View { @State private var viewModel: ProxySettingsViewModel + private var proxyHostTitle: String { + languageSettings.localized("Main settings proxy host") + } + + private var proxyPortTitle: String { + languageSettings.localized("Main settings proxy port") + } + + private var proxyUsernameTitle: String { + languageSettings.localized("Main settings proxy username") + } + + private var proxyPasswordTitle: String { + languageSettings.localized("Main settings proxy password") + } + init() { _viewModel = State(wrappedValue: Container.shared.proxySettingsViewModel()) } @@ -110,22 +126,26 @@ struct ProxySettingsView: View { @ViewBuilder private var manualCardContent: some View { FloatingLabelTextField( - title: languageSettings.localized("Main settings proxy host"), + title: proxyHostTitle, + placeholder: proxyHostTitle, text: $viewModel.proxyInfo.host, ) FloatingLabelTextField( - title: languageSettings.localized("Main settings proxy port"), + title: proxyPortTitle, + placeholder: proxyPortTitle, text: $viewModel.portText, isError: !viewModel.isPortTextValid, errorText: languageSettings.localized("Main settings proxy port error"), keyboardType: .numberPad ) FloatingLabelTextField( - title: languageSettings.localized("Main settings proxy username"), + title: proxyUsernameTitle, + placeholder: proxyUsernameTitle, text: $viewModel.proxyInfo.username, ) FloatingLabelTextField( - title: languageSettings.localized("Main settings proxy password"), + title: proxyPasswordTitle, + placeholder: proxyPasswordTitle, text: $viewModel.proxyInfo.password, isSecure: true ) diff --git a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift index da6317f2..d604ae14 100644 --- a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift +++ b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift @@ -438,6 +438,7 @@ struct FloatingLabelTextField: View { VStack(spacing: 20) { FloatingLabelTextField( title: "field label", + placeholder: "field placeholder", text: .constant("text inside field") ) diff --git a/RIADigiDoc/UI/Component/SigningServicesSettingsView/MobileIDSmartIDSettingsView.swift b/RIADigiDoc/UI/Component/SigningServicesSettingsView/MobileIDSmartIDSettingsView.swift index 0f4dffe3..1b454363 100644 --- a/RIADigiDoc/UI/Component/SigningServicesSettingsView/MobileIDSmartIDSettingsView.swift +++ b/RIADigiDoc/UI/Component/SigningServicesSettingsView/MobileIDSmartIDSettingsView.swift @@ -28,6 +28,10 @@ struct MobileIDSmartIDSettingsView: View { @State private var viewModel: MobileIDSmartIDSettingsViewModel + private var relyingPartyTitle: String { + languageSettings.localized("Main settings relying party title") + } + init() { _viewModel = State(wrappedValue: Container.shared.mobileIDSmartIDSettingsViewModel()) } @@ -54,7 +58,8 @@ struct MobileIDSmartIDSettingsView: View { accessibilityInputLabel: .manualSetting, content: { FloatingLabelTextField( - title: languageSettings.localized("Main settings relying party title"), + title: relyingPartyTitle, + placeholder: relyingPartyTitle, text: $viewModel.relyingPartyUUID, isSecure: true, ) diff --git a/RIADigiDoc/ViewModel/CryptoHomeViewModel.swift b/RIADigiDoc/ViewModel/CryptoHomeViewModel.swift index de83215f..f5bf0fdd 100644 --- a/RIADigiDoc/ViewModel/CryptoHomeViewModel.swift +++ b/RIADigiDoc/ViewModel/CryptoHomeViewModel.swift @@ -62,7 +62,7 @@ class CryptoHomeViewModel: CryptoHomeViewModelProtocol { func getRecentDocumentsFolder() -> URL? { do { return try Directories.getCacheDirectory(fileManager: fileManager) - .appending(path: Constants.Container.CryptoContainerFolder) + .appending(path: Constants.Folder.CryptoContainerFolder) } catch { CryptoHomeViewModel.logger.error("Unable to get crypto recent documents folder: \(error)") return nil diff --git a/RIADigiDoc/ViewModel/DiagnosticsViewModel.swift b/RIADigiDoc/ViewModel/DiagnosticsViewModel.swift index 4347302a..15db62b9 100644 --- a/RIADigiDoc/ViewModel/DiagnosticsViewModel.swift +++ b/RIADigiDoc/ViewModel/DiagnosticsViewModel.swift @@ -36,7 +36,7 @@ class DiagnosticsViewModel: DiagnosticsViewModelProtocol { var versionSectionContent: String = "" var osSectionContent: (key: String, content: String) = (key: "", content: "") var libdigidocVersion: String = "" - var urlSectionContent: [String] = [""] + var urlSectionContent: [(key: String, content: String)] = [(key: "", content: "")] var cdoc2SectionContent: [String] = [""] var tslSectionContent: [String] = [""] var centralConfigurationSectionContent: [(key: String, content: String)] = [(key: "", content: "")] @@ -118,7 +118,9 @@ class DiagnosticsViewModel: DiagnosticsViewModelProtocol { private func loadUrlSectionContent(configuration: ConfigurationProvider?) async { guard let config = configuration else { return } - let lines: [(label: String, value: String)] = await [ + let rpUuid = await getRpUuid() + + self.urlSectionContent = [ ("CONFIG_URL", config.metaInf.url), ("TSL_URL", config.tslUrl.absoluteString), ("SIVA_URL", config.sivaUrl.absoluteString), @@ -129,10 +131,11 @@ class DiagnosticsViewModel: DiagnosticsViewModelProtocol { ("MID_SK_URL", config.midSkRestUrl.absoluteString), ("SIDV2_PROXY_URL", config.sidV2RestUrl.absoluteString), ("SIDV2_SK_URL", config.sidV2SkRestUrl.absoluteString), - ("RPUUID", getRpUuid()) + ("RPUUID", rpUuid == Constants.Signing.RelyingPartyUUID + ? "Main diagnostics rpuuid default" + : rpUuid + ) ] - - self.urlSectionContent = lines.map { "\($0.label): \($0.value)" } } private func loadCdoc2SectionContent(configuration: ConfigurationProvider?) { @@ -267,7 +270,9 @@ class DiagnosticsViewModel: DiagnosticsViewModelProtocol { lines.append("") lines.append(languageSettings.localized("Main diagnostics urls title")) - lines.append(contentsOf: self.urlSectionContent) + lines.append(contentsOf: self.urlSectionContent.map { + "\($0.key): \(languageSettings.localized($0.content))" + }) lines.append("") lines.append(languageSettings.localized("Main diagnostics cdoc2 title")) diff --git a/RIADigiDoc/ViewModel/HomeViewModel.swift b/RIADigiDoc/ViewModel/HomeViewModel.swift index 58eb9d2b..f42191bf 100644 --- a/RIADigiDoc/ViewModel/HomeViewModel.swift +++ b/RIADigiDoc/ViewModel/HomeViewModel.swift @@ -62,7 +62,7 @@ class HomeViewModel: HomeViewModelProtocol { func getRecentDocumentsFolder() -> URL? { do { return try Directories.getCacheDirectory(fileManager: fileManager) - .appending(path: Constants.Container.SignedContainerFolder) + .appending(path: Constants.Folder.SignedContainerFolder) } catch { HomeViewModel.logger.error("Unable to get signed containers recent documents folder: \(error)") return nil diff --git a/RIADigiDoc/ViewModel/Protocols/DiagnosticsViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/DiagnosticsViewModelProtocol.swift index 3adc619b..f755a034 100644 --- a/RIADigiDoc/ViewModel/Protocols/DiagnosticsViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/DiagnosticsViewModelProtocol.swift @@ -28,7 +28,7 @@ public protocol DiagnosticsViewModelProtocol: Sendable { var versionSectionContent: String { get } var osSectionContent: (key: String, content: String) { get } var libdigidocVersion: String { get } - var urlSectionContent: [String] { get } + var urlSectionContent: [(key: String, content: String)] { get } var cdoc2SectionContent: [String] { get } var tslSectionContent: [String] { get } var centralConfigurationSectionContent: [(key: String, content: String)] { get } From 8b7655b774ad631743d07000873ad1d85c04f9cc Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 5 Dec 2025 02:35:22 +0200 Subject: [PATCH 2/8] Fix tests --- .../CentralConfigurationServiceTests.swift | 6 +++--- .../Mocks/TestConfigurationProvider.swift | 1 - .../TestConfigurationProviderUtil.swift | 1 - .../Container/ContainerUtilTests.swift | 18 ++++++++--------- .../ViewModel/DiagnosticsViewModelTests.swift | 19 +++++++----------- .../ViewModel/SigningViewModelTests.swift | 20 +++++++++++++++++-- 6 files changed, 37 insertions(+), 28 deletions(-) diff --git a/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Service/CentralConfigurationServiceTests.swift b/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Service/CentralConfigurationServiceTests.swift index c8d80218..d99b6f85 100644 --- a/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Service/CentralConfigurationServiceTests.swift +++ b/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Service/CentralConfigurationServiceTests.swift @@ -105,7 +105,7 @@ struct CentralConfigurationServiceTests { session: session ) - await #expect(throws: Alamofire.AFError.self) { + await #expect(throws: Error.self) { try await errorService.fetchConfiguration(proxyInfo: ProxyInfo()) } } @@ -175,7 +175,7 @@ struct CentralConfigurationServiceTests { session: session ) - await #expect(throws: Alamofire.AFError.self) { + await #expect(throws: Error.self) { try await errorService.fetchPublicKey(proxyInfo: ProxyInfo()) } } @@ -245,7 +245,7 @@ struct CentralConfigurationServiceTests { session: session ) - await #expect(throws: Alamofire.AFError.self) { + await #expect(throws: Error.self) { try await errorService.fetchSignature(proxyInfo: ProxyInfo()) } } diff --git a/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift b/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift index a84336d1..b57f5d5f 100644 --- a/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift +++ b/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift @@ -103,7 +103,6 @@ public class TestConfigurationProvider { tslUrl: tslURL, tslCerts: tslCertsData, tsaUrl: tsaURL, - ocspIssuers: ocspIssuers, ldapPersonUrls: ldapPersonURLs, ldapPersonUrl: ldapPersonURL, ldapCorpUrl: ldapCorpURL, diff --git a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SharedTest/TestConfigurationProviderUtil.swift b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SharedTest/TestConfigurationProviderUtil.swift index 8d221e4e..230079cd 100644 --- a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SharedTest/TestConfigurationProviderUtil.swift +++ b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SharedTest/TestConfigurationProviderUtil.swift @@ -49,7 +49,6 @@ public struct TestConfigurationProviderUtil { tslUrl: tslUrl, tslCerts: [], tsaUrl: tsaUrl, - ocspIssuers: [:], ldapPersonUrls: [], ldapPersonUrl: ldapPersonUrl, ldapCorpUrl: ldapCorpUrl, diff --git a/Modules/UtilsLib/Tests/UtilsLibTests/Container/ContainerUtilTests.swift b/Modules/UtilsLib/Tests/UtilsLibTests/Container/ContainerUtilTests.swift index 51843669..c78d23be 100644 --- a/Modules/UtilsLib/Tests/UtilsLibTests/Container/ContainerUtilTests.swift +++ b/Modules/UtilsLib/Tests/UtilsLibTests/Container/ContainerUtilTests.swift @@ -72,7 +72,7 @@ struct ContainerUtilTests { in: tempDirectory ) - #expect(uniqueFileURL.lastPathComponent == "\(fileURL.deletingPathExtension().lastPathComponent)-1.txt") + #expect(uniqueFileURL.lastPathComponent == "\(fileURL.deletingPathExtension().lastPathComponent) (1).txt") } @Test @@ -84,8 +84,8 @@ struct ContainerUtilTests { let existingPaths: Set = [ fileURL.resolvedPath, - tempDirectory.appending(path: "\(uniqueFileName)-1.txt").resolvedPath, - tempDirectory.appending(path: "\(uniqueFileName)-2.txt").resolvedPath + tempDirectory.appending(path: "\(uniqueFileName) (1).txt").resolvedPath, + tempDirectory.appending(path: "\(uniqueFileName) (2).txt").resolvedPath ] mockFileManager.fileExistsHandler = { path in @@ -97,7 +97,7 @@ struct ContainerUtilTests { in: tempDirectory ) - #expect(uniqueFileURL.lastPathComponent == "\(fileURL.deletingPathExtension().lastPathComponent)-3.txt") + #expect(uniqueFileURL.lastPathComponent == "\(fileURL.deletingPathExtension().lastPathComponent) (3).txt") } @Test @@ -119,7 +119,7 @@ struct ContainerUtilTests { in: tempDirectory ) - #expect(uniqueFileURL.lastPathComponent == "\(uniqueFileName)-1") + #expect(uniqueFileURL.lastPathComponent == "\(uniqueFileName) (1)") } @Test @@ -143,7 +143,7 @@ struct ContainerUtilTests { in: tempDirectory ) - #expect(uniqueFileURL.lastPathComponent == "\(fileURL.deletingPathExtension().lastPathComponent)-1.txt") + #expect(uniqueFileURL.lastPathComponent == "\(fileURL.deletingPathExtension().lastPathComponent) (1).txt") } @Test @@ -151,7 +151,7 @@ struct ContainerUtilTests { let cachesDir = URL(fileURLWithPath: "/mock/cache") let expectedDir = cachesDir .appending(path: BundleUtil.getBundleIdentifier()) - .appending(path: Constants.Container.SignedContainerFolder) + .appending(path: Constants.Folder.SignedContainerFolder) 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.Container.SignedContainerFolder) + let signatureDir = cachesDir.appending(path: Constants.Folder.SignedContainerFolder) let containerFile = signatureDir.appending(path: "file.asice") let expectedDataDir = cachesDir - .appending(path: Constants.Container.SignedContainerFolder) + .appending(path: Constants.Folder.SignedContainerFolder) .appending(path: "file.asice-data-files") mockFileManager.urlHandler = { _, _, _, _ in cachesDir } diff --git a/RIADigiDocTests/ViewModel/DiagnosticsViewModelTests.swift b/RIADigiDocTests/ViewModel/DiagnosticsViewModelTests.swift index 50c6cd5a..aaac84a2 100644 --- a/RIADigiDocTests/ViewModel/DiagnosticsViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/DiagnosticsViewModelTests.swift @@ -72,15 +72,6 @@ final class DiagnosticsViewModelTests { ) } - private static func mockAsyncStream( - configProvider: ConfigurationProvider - ) -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - continuation.yield(configProvider) - continuation.finish() - } - } - // MARK: - Get Configuration Data Tests @Test @@ -138,7 +129,9 @@ final class DiagnosticsViewModelTests { } private func checkUrlSection() async { - #expect(viewModel.urlSectionContent == [ + let sectionContent = viewModel.urlSectionContent.map { "\($0.key): \($0.content)" } + + let expected = [ "CONFIG_URL: https://someUrl.abc", "TSL_URL: https://tsl.someUrl.abc", "SIVA_URL: https://siva.someUrl.abc", @@ -149,8 +142,10 @@ final class DiagnosticsViewModelTests { "MID_SK_URL: https://midskrest.someUrl.abc", "SIDV2_PROXY_URL: https://sidv2.someUrl.abc", "SIDV2_SK_URL: https://sidv2skrest.someUrl.abc", - "RPUUID: 00000000-0000-0000-0000-000000000000" - ]) + "RPUUID: Main diagnostics rpuuid default" + ] + + #expect(expected == sectionContent) } private func checkCdoc2Section() async { diff --git a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift index 5f65c580..4b73d3c7 100644 --- a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift @@ -1319,9 +1319,18 @@ struct SigningViewModelTests { await viewModel.loadContainerData(signedContainer: signedContainer) + let rawContainerFile = await signedContainer.getRawContainerFile() + + guard let containerFile = rawContainerFile else { + Issue.record("Unable to get raw container file") + return + } + await viewModel.addDataFiles([ testFile, testFile2, testFile3 - ], to: localExampleContainer) + ], to: containerFile) + + print(signedContainer) #expect(viewModel.errorMessage == ErrorMessage(key: "Could not add files", args: ["2"])) #expect(viewModel.dataFiles.count == 3) @@ -1367,9 +1376,16 @@ struct SigningViewModelTests { await viewModel.loadContainerData(signedContainer: signedContainer) + let rawContainerFile = await signedContainer.getRawContainerFile() + + guard let containerFile = rawContainerFile else { + Issue.record("Unable to get raw container file") + return + } + await viewModel.addDataFiles([ testFile, testFile2 - ], to: localExampleContainer) + ], to: containerFile) #expect(viewModel.errorMessage == ErrorMessage(key: "Could not add files", args: ["2"])) #expect(viewModel.dataFiles.count == 2) From 12fd6560efb60aa157404f04112a29840d07da63 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 5 Dec 2025 03:21:17 +0200 Subject: [PATCH 3/8] Remove OCSPIssuers --- .../Configuration/Provider/ConfigurationProvider.swift | 6 ------ .../LibdigidocObjC/include/Conf/DigiDocConfWrapper.mm | 9 --------- .../Sources/LibdigidocObjC/include/Model/DigiDocConfig.h | 2 -- .../LibdigidocObjC/include/Model/DigiDocConfig.mm | 2 -- .../LibdigidocSwift/Domain/Conf/DigiDocConf.swift | 5 ----- 5 files changed, 24 deletions(-) diff --git a/Modules/ConfigLib/Sources/ConfigLib/Configuration/Provider/ConfigurationProvider.swift b/Modules/ConfigLib/Sources/ConfigLib/Configuration/Provider/ConfigurationProvider.swift index 4bdebac4..31504636 100644 --- a/Modules/ConfigLib/Sources/ConfigLib/Configuration/Provider/ConfigurationProvider.swift +++ b/Modules/ConfigLib/Sources/ConfigLib/Configuration/Provider/ConfigurationProvider.swift @@ -67,7 +67,6 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { public let tslUrl: URL public let tslCerts: [Data] public let tsaUrl: URL - public let ocspIssuers: [String: String] public let ldapPersonUrls: [URL] public let ldapPersonUrl: URL public let ldapCorpUrl: URL @@ -90,7 +89,6 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { case tslUrl = "TSL-URL" case tslCerts = "TSL-CERTS" case tsaUrl = "TSA-URL" - case ocspIssuers = "OCSP-URL-ISSUER" case ldapPersonUrls = "LDAP-PERSON-URLS" case ldapPersonUrl = "LDAP-PERSON-URL" case ldapCorpUrl = "LDAP-CORP-URL" @@ -123,7 +121,6 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { try container.encode(tslUrl, forKey: .tslUrl) try container.encode(tslCerts, forKey: .tslCerts) try container.encode(tsaUrl, forKey: .tsaUrl) - try container.encode(ocspIssuers, forKey: .ocspIssuers) try container.encode(ldapPersonUrl, forKey: .ldapPersonUrl) try container.encode(ldapPersonUrls, forKey: .ldapPersonUrls) try container.encode(ldapCorpUrl, forKey: .ldapCorpUrl) @@ -147,7 +144,6 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { tslUrl = try container.decode(URL.self, forKey: .tslUrl) tslCerts = try container.decode([Data].self, forKey: .tslCerts) tsaUrl = try container.decode(URL.self, forKey: .tsaUrl) - ocspIssuers = try container.decode([String: String].self, forKey: .ocspIssuers) ldapPersonUrls = try container.decode([URL].self, forKey: .ldapPersonUrls) ldapPersonUrl = try container.decode(URL.self, forKey: .ldapPersonUrl) ldapCorpUrl = try container.decode(URL.self, forKey: .ldapCorpUrl) @@ -178,7 +174,6 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { tslUrl: URL, tslCerts: [Data], tsaUrl: URL, - ocspIssuers: [String: String], ldapPersonUrls: [URL], ldapPersonUrl: URL, ldapCorpUrl: URL, @@ -200,7 +195,6 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { self.tslUrl = tslUrl self.tslCerts = tslCerts self.tsaUrl = tsaUrl - self.ocspIssuers = ocspIssuers self.ldapPersonUrls = ldapPersonUrls self.ldapPersonUrl = ldapPersonUrl self.ldapCorpUrl = ldapCorpUrl diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Conf/DigiDocConfWrapper.mm b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Conf/DigiDocConfWrapper.mm index b00b0ce3..520a01e2 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Conf/DigiDocConfWrapper.mm +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Conf/DigiDocConfWrapper.mm @@ -121,15 +121,6 @@ void setProxyPass(NSString *proxyPass) { return digidoc::ConfCurrent::TSUrl(); } - - std::string ocsp(const std::string &issuer) const final { - NSString *ocspIssuer = [NSString stringWithUTF8String:issuer.c_str()]; - NSString *ocspUrl = currentConf.OCSPISSUERS[ocspIssuer]; - if (ocspUrl != nil && ocspUrl.length > 0) { - return ocspUrl.UTF8String; - } - return digidoc::ConfCurrent::ocsp(issuer); - } std::string proxyHost() const final { if (_proxyHost && _proxyHost.length > 0) { diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocConfig.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocConfig.h index 96a23b9e..3a7d0ef0 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocConfig.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocConfig.h @@ -31,7 +31,6 @@ @property (nonatomic, strong) NSArray *TSLCERTS; @property (nonatomic, strong) NSArray *LDAPCERTS; @property (nonatomic, strong) NSURL *TSAURL; -@property (nonatomic, strong) NSDictionary *OCSPISSUERS; @property (nonatomic, strong) NSArray *CERTBUNDLE; - (instancetype)initWithConf:(int)logLevel @@ -42,7 +41,6 @@ TSLCERTS:(NSArray *)TSLCERTS LDAPCERTS:(NSArray *)LDAPCERTS TSAURL:(NSURL *)TSAURL - OCSPISSUERS:(NSDictionary *)OCSPISSUERS CERTBUNDLE:(NSArray *)CERTBUNDLE; @end diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocConfig.mm b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocConfig.mm index 3c29ff04..cc48b8b6 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocConfig.mm +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocConfig.mm @@ -30,7 +30,6 @@ - (instancetype)initWithConf:(int)logLevel TSLCERTS:(NSArray *)TSLCERTS LDAPCERTS:(NSArray *)LDAPCERTS TSAURL:(NSURL *)TSAURL - OCSPISSUERS:(NSDictionary *)OCSPISSUERS CERTBUNDLE:(NSArray *)CERTBUNDLE { self = [super init]; if (self) { @@ -42,7 +41,6 @@ - (instancetype)initWithConf:(int)logLevel _TSLCERTS = TSLCERTS; _LDAPCERTS = LDAPCERTS; _TSAURL = TSAURL; - _OCSPISSUERS = OCSPISSUERS; _CERTBUNDLE = CERTBUNDLE; } return self; diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Conf/DigiDocConf.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Conf/DigiDocConf.swift index f399f7dd..4a9c05ec 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Conf/DigiDocConf.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Conf/DigiDocConf.swift @@ -220,7 +220,6 @@ public actor DigiDocInitializer { digiDocConfiguration.tslcerts = overrideTSLCerts(conf: configurationProvider) digiDocConfiguration.tsaurl = overrideTSAUrl(conf: configurationProvider) digiDocConfiguration.sivaurl = overrideSiVaUrl(conf: configurationProvider) - digiDocConfiguration.ocspissuers = overrideOCSPIssuers(conf: configurationProvider) digiDocConfiguration.certbundle = overrideCertBundle(conf: configurationProvider) digiDocConfiguration.ldapcerts = overrideLDAPCerts(conf: configurationProvider) @@ -284,10 +283,6 @@ public actor DigiDocInitializer { return conf.sivaUrl } - private func overrideOCSPIssuers(conf: ConfigurationProvider) -> [String: String] { - return conf.ocspIssuers - } - private func overrideCertBundle(conf: ConfigurationProvider) -> [Data] { return conf.certBundle } From e5286975362aa3a8c934f431b8af955a6d4c3199 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 5 Dec 2025 03:21:52 +0200 Subject: [PATCH 4/8] Add Diagnostics accessibility identifier --- .../DiagnosticsView/DiagnosticsSections.swift | 13 ++++++-- .../DiagnosticsSingleSection.swift | 31 ++++++++++++++----- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSections.swift b/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSections.swift index fcf53c16..75c4d5ec 100644 --- a/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSections.swift +++ b/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSections.swift @@ -36,41 +36,48 @@ struct DiagnosticsSections: View { VStack { DiagnosticsSingleSection( title: languageSettings.localized("Main diagnostics application version title"), - content: versionSectionContent + content: versionSectionContent, + identifier: "applicationVersion" ) DiagnosticsSingleSection( title: languageSettings.localized(osSectionContent.key), content: osSectionContent.content, + identifier: "osVersion", ) DiagnosticsSingleSection( title: languageSettings.localized("Main diagnostics libraries title"), - content: libdigidocVersion + content: libdigidocVersion, + identifier: "library" ) DiagnosticsSingleSection( title: languageSettings.localized("Main diagnostics urls title"), contentLines: urlSectionContent .map { "\($0.key): \(languageSettings.localized($0.content))" }, + identifier: "url", showDivider: false, ) DiagnosticsSingleSection( title: languageSettings.localized("Main diagnostics cdoc2 title"), contentLines: cdoc2SectionContent, + identifier: "cdoc2", ) DiagnosticsSingleSection( title: languageSettings.localized("Main diagnostics tsl cache title"), contentLines: tslSectionContent, + identifier: "tslCache", ) DiagnosticsSingleSection( title: languageSettings.localized("Main diagnostics central configuration title"), contentLines: centralConfigurationSectionContent .map { "\(languageSettings.localized($0.key)): \($0.content)" - } + }, + identifier: "centralConfiguration" ) } } diff --git a/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSingleSection.swift b/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSingleSection.swift index 5782f906..c42cec45 100644 --- a/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSingleSection.swift +++ b/RIADigiDoc/UI/Component/DiagnosticsView/DiagnosticsSingleSection.swift @@ -26,6 +26,7 @@ struct DiagnosticsSingleSection: View { let title: String let contentLines: [String] + let identifier: String var showDivider: Bool = true var body: some View { @@ -35,10 +36,20 @@ struct DiagnosticsSingleSection: View { .foregroundStyle(theme.onSurface) .font(typography.titleMedium) - ForEach(contentLines, id: \.self) { line in - Text(verbatim: line) - .font(typography.bodyMedium) - .foregroundColor(theme.onSurfaceVariant) + if #available(iOS 26.0, *) { + ForEach(contentLines.enumerated(), id: \.offset) { index, line in + Text(verbatim: line) + .font(typography.bodyMedium) + .foregroundColor(theme.onSurfaceVariant) + .accessibilityIdentifier("\(identifier)-\(index + 1)") + } + } else { + ForEach(Array(contentLines.enumerated()), id: \.offset) { index, line in + Text(verbatim: line) + .font(typography.bodyMedium) + .foregroundColor(theme.onSurfaceVariant) + .accessibilityIdentifier("\(identifier)-\(index + 1)") + } } } @@ -54,9 +65,10 @@ struct DiagnosticsSingleSection: View { // MARK: - Initializer for String input extension DiagnosticsSingleSection { - init(title: String, content: String, showDivider: Bool = true) { + init(title: String, content: String, identifier: String, showDivider: Bool = true) { self.title = title self.contentLines = content.components(separatedBy: .newlines) + self.identifier = identifier self.showDivider = showDivider } } @@ -66,17 +78,20 @@ extension DiagnosticsSingleSection { VStack { DiagnosticsSingleSection( title: "Section with Array", - contentLines: ["Line 1", "Line 2", "Line 3"] + contentLines: ["Line 1", "Line 2", "Line 3"], + identifier: "sectionWithArray" ) DiagnosticsSingleSection( title: "Section with String", - content: "Line 1\nLine 2\nLine 3" + content: "Line 1\nLine 2\nLine 3", + identifier: "sectionWithString" ) DiagnosticsSingleSection( title: "Section with String", - content: "Line 1" + content: "Line 1", + identifier: "sectionWithString" ) } .environment(Container.shared.languageSettings()) From 6a6a4e634b3cb3c641feb9fd2f5bd0c93767d5dd Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Mon, 8 Dec 2025 15:50:02 +0200 Subject: [PATCH 5/8] TextField accessibility improvements --- .../AdvancedSettingsManualCardContent.swift | 6 +++++- .../Signing/MobileId/MobileIdInputView.swift | 6 ++++-- .../Container/Signing/RoleView/RoleView.swift | 15 ++++++++++----- .../Signing/SmartId/SmartIdInputView.swift | 6 ++++-- .../UI/Component/EncryptionSettingsView.swift | 12 ++++++++---- RIADigiDoc/UI/Component/ProxySettingsView.swift | 8 ++++++-- .../Shared/FloatingLabelTextField.swift | 16 +++++++++++++--- .../MobileIDSmartIDSettingsView.swift | 1 + .../TimeStampSettingsView.swift | 3 ++- .../UI/Component/ValidationSettingsView.swift | 3 ++- 10 files changed, 55 insertions(+), 21 deletions(-) diff --git a/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsManualCardContent.swift b/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsManualCardContent.swift index 223cc39d..7de38e1e 100644 --- a/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsManualCardContent.swift +++ b/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsManualCardContent.swift @@ -39,6 +39,8 @@ struct AdvancedSettingsManualCardContent: View { var onShowCertificatePressed: () -> Void var onAddCertificatePressed: () -> Void + var identifier: String + var body: some View { VStack( spacing: Dimensions.Padding.LPadding, @@ -47,6 +49,7 @@ struct AdvancedSettingsManualCardContent: View { title: textFieldTitle, placeholder: textFieldTitle, text: $textFieldText, + identifier: identifier ) AdvancedSettingsCertificateSection( certificateInfoHeader: certificateInfoHeader, @@ -71,7 +74,8 @@ struct AdvancedSettingsManualCardContent: View { certificateIssuedTo: "", certificateValidTo: "", onShowCertificatePressed: {}, - onAddCertificatePressed: {} + onAddCertificatePressed: {}, + identifier: "certificateHeader" ) .padding() .environment(Container.shared.languageSettings()) diff --git a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdInputView.swift b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdInputView.swift index 488bf8fd..c1e3bfb7 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdInputView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdInputView.swift @@ -82,7 +82,8 @@ struct MobileIdInputView: View { isError: !countryCodeAndPhoneErrorText.isEmpty, errorText: countryCodeAndPhoneErrorText, keyboardType: .phonePad, - showDashButton: true + showDashButton: true, + identifier: "mobileIdCountryCodeAndPhoneNumber" ) .onChange(of: phoneNumber) { onInputChange() @@ -97,7 +98,8 @@ struct MobileIdInputView: View { isError: !personalCodeErrorText.isEmpty, errorText: personalCodeErrorText, keyboardType: .phonePad, - showDashButton: true + showDashButton: true, + identifier: "mobileIdPersonalCode" ) .onChange(of: personalCode) { onInputChange() diff --git a/RIADigiDoc/UI/Component/Container/Signing/RoleView/RoleView.swift b/RIADigiDoc/UI/Component/Container/Signing/RoleView/RoleView.swift index 33190ca1..f47a34e8 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/RoleView/RoleView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/RoleView/RoleView.swift @@ -94,27 +94,32 @@ struct RoleView: View { FloatingLabelTextField( title: roleTitle, placeholder: roleTitle, - text: $roles + text: $roles, + identifier: "roles" ) FloatingLabelTextField( title: cityTitle, placeholder: cityTitle, - text: $city + text: $city, + identifier: "roleCity" ) FloatingLabelTextField( title: stateTitle, placeholder: stateTitle, - text: $state + text: $state, + identifier: "roleState" ) FloatingLabelTextField( title: countryTitle, placeholder: countryTitle, - text: $country + text: $country, + identifier: "roleCountry" ) FloatingLabelTextField( title: zipCodeTitle, placeholder: zipCodeTitle, - text: $zipCode + text: $zipCode, + identifier: "roleZipCode" ) PrimaryButton( diff --git a/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdInputView.swift b/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdInputView.swift index ae6bb4f8..fb46706c 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdInputView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdInputView.swift @@ -80,7 +80,8 @@ struct SmartIdInputView: View { isDisabled: false, onDropdownTap: { showCountryChooser = true - } + }, + identifier: "smartIdCountry" ) } @@ -92,7 +93,8 @@ struct SmartIdInputView: View { isError: !personalCodeErrorText.isEmpty, errorText: personalCodeErrorText, keyboardType: .phonePad, - showDashButton: true + showDashButton: true, + identifier: "smartIdPersonalCode" ) .onChange(of: personalCode) { onInputChange() diff --git a/RIADigiDoc/UI/Component/EncryptionSettingsView.swift b/RIADigiDoc/UI/Component/EncryptionSettingsView.swift index 4f62d881..903200bb 100644 --- a/RIADigiDoc/UI/Component/EncryptionSettingsView.swift +++ b/RIADigiDoc/UI/Component/EncryptionSettingsView.swift @@ -144,7 +144,8 @@ struct EncryptionSettingsView: View { dialogSelectedServerId = viewModel.serverId showDialog = true isDialogHeaderFocused = true - } + }, + identifier: "cryptoServer" ) FloatingLabelTextField( @@ -152,21 +153,24 @@ struct EncryptionSettingsView: View { placeholder: cryptoUuidTitle, text: $viewModel.serverInfo.uuid, isDisabled: !viewModel.useKeyTransfer - || viewModel.serverId != viewModel.cdoc2ManualKeyTransferServerUUID + || viewModel.serverId != viewModel.cdoc2ManualKeyTransferServerUUID, + identifier: "cryptoUuid" ) FloatingLabelTextField( title: cryptoFetchUrlTitle, placeholder: cryptoFetchUrlTitle, text: $viewModel.serverInfo.fetchURL, isDisabled: !viewModel.useKeyTransfer - || viewModel.serverId != viewModel.cdoc2ManualKeyTransferServerUUID + || viewModel.serverId != viewModel.cdoc2ManualKeyTransferServerUUID, + identifier: "cryptoFetchUrl" ) FloatingLabelTextField( title: cryptoPostUrlTitle, placeholder: cryptoPostUrlTitle, text: $viewModel.serverInfo.postURL, isDisabled: !viewModel.useKeyTransfer - || viewModel.serverId != viewModel.cdoc2ManualKeyTransferServerUUID + || viewModel.serverId != viewModel.cdoc2ManualKeyTransferServerUUID, + identifier: "cryptoPostUrl" ) if viewModel.serverId == viewModel.cdoc2ManualKeyTransferServerUUID { AdvancedSettingsCertificateSection( diff --git a/RIADigiDoc/UI/Component/ProxySettingsView.swift b/RIADigiDoc/UI/Component/ProxySettingsView.swift index cc9375ad..db4aaad7 100644 --- a/RIADigiDoc/UI/Component/ProxySettingsView.swift +++ b/RIADigiDoc/UI/Component/ProxySettingsView.swift @@ -129,6 +129,7 @@ struct ProxySettingsView: View { title: proxyHostTitle, placeholder: proxyHostTitle, text: $viewModel.proxyInfo.host, + identifier: "proxyHost" ) FloatingLabelTextField( title: proxyPortTitle, @@ -136,18 +137,21 @@ struct ProxySettingsView: View { text: $viewModel.portText, isError: !viewModel.isPortTextValid, errorText: languageSettings.localized("Main settings proxy port error"), - keyboardType: .numberPad + keyboardType: .numberPad, + identifier: "proxyPort" ) FloatingLabelTextField( title: proxyUsernameTitle, placeholder: proxyUsernameTitle, text: $viewModel.proxyInfo.username, + identifier: "proxyUsername" ) FloatingLabelTextField( title: proxyPasswordTitle, placeholder: proxyPasswordTitle, text: $viewModel.proxyInfo.password, - isSecure: true + isSecure: true, + identifier: "proxyPassword" ) } } diff --git a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift index d604ae14..ac11fc0c 100644 --- a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift +++ b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift @@ -43,6 +43,7 @@ struct FloatingLabelTextField: View { let submitLabel: SubmitLabel let keyboardType: UIKeyboardType let showDashButton: Bool + let identifier: String init( title: String, @@ -56,7 +57,8 @@ struct FloatingLabelTextField: View { errorText: String = "", submitLabel: SubmitLabel = .done, keyboardType: UIKeyboardType = .default, - showDashButton: Bool = false + showDashButton: Bool = false, + identifier: String = "" ) { self.title = title self.placeholder = placeholder @@ -70,6 +72,7 @@ struct FloatingLabelTextField: View { self.submitLabel = submitLabel self.keyboardType = keyboardType self.showDashButton = showDashButton + self.identifier = identifier } // MARK: - State @@ -140,6 +143,13 @@ struct FloatingLabelTextField: View { } } + private var textFieldAccessibility: String { + let components = [title, text, errorText] + .filter { !$0.isEmpty } + + return components.joined(separator: " ") + } + // MARK: - Body var body: some View { @@ -207,7 +217,7 @@ struct FloatingLabelTextField: View { ) .buttonStyle(.plain) .disabled(isDisabled) - .accessibilityLabel(Text(verbatim: "\(title) \(text) \(errorText)")) + .accessibilityLabel(Text(verbatim: textFieldAccessibility)) .accessibilityValue(Text(verbatim: "")) } @@ -217,7 +227,7 @@ struct FloatingLabelTextField: View { private var inputContainer: some View { HStack(spacing: Dimensions.Padding.XSPadding) { inputField - .accessibilityLabel(Text(verbatim: "\(title) \(text) \(errorText)")) + .accessibilityLabel(Text(verbatim: textFieldAccessibility)) .accessibilityValue(Text(verbatim: "")) Spacer() trailingIcon diff --git a/RIADigiDoc/UI/Component/SigningServicesSettingsView/MobileIDSmartIDSettingsView.swift b/RIADigiDoc/UI/Component/SigningServicesSettingsView/MobileIDSmartIDSettingsView.swift index 1b454363..c9e1b33d 100644 --- a/RIADigiDoc/UI/Component/SigningServicesSettingsView/MobileIDSmartIDSettingsView.swift +++ b/RIADigiDoc/UI/Component/SigningServicesSettingsView/MobileIDSmartIDSettingsView.swift @@ -62,6 +62,7 @@ struct MobileIDSmartIDSettingsView: View { placeholder: relyingPartyTitle, text: $viewModel.relyingPartyUUID, isSecure: true, + identifier: "relyingPartyUUID" ) } ) diff --git a/RIADigiDoc/UI/Component/SigningServicesSettingsView/TimeStampSettingsView.swift b/RIADigiDoc/UI/Component/SigningServicesSettingsView/TimeStampSettingsView.swift index 5c28f702..89adc527 100644 --- a/RIADigiDoc/UI/Component/SigningServicesSettingsView/TimeStampSettingsView.swift +++ b/RIADigiDoc/UI/Component/SigningServicesSettingsView/TimeStampSettingsView.swift @@ -72,7 +72,8 @@ struct TimeStampSettingsView: View { }, onAddCertificatePressed: { viewModel.isImportingTSACert = true - } + }, + identifier: "tsaUrl" ) } } diff --git a/RIADigiDoc/UI/Component/ValidationSettingsView.swift b/RIADigiDoc/UI/Component/ValidationSettingsView.swift index f2e0252b..fe4505c6 100644 --- a/RIADigiDoc/UI/Component/ValidationSettingsView.swift +++ b/RIADigiDoc/UI/Component/ValidationSettingsView.swift @@ -82,7 +82,8 @@ struct ValidationSettingsView: View { }, onAddCertificatePressed: { viewModel.isImportingCert = true - } + }, + identifier: "sivaUrl" ) } From 0eff8373a06624981c591f02025734f00d2a02bf Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Tue, 9 Dec 2025 18:25:01 +0200 Subject: [PATCH 6/8] Add accessibility identifier to signing methods --- RIADigiDoc/UI/Component/Shared/RadioButtonChooserView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RIADigiDoc/UI/Component/Shared/RadioButtonChooserView.swift b/RIADigiDoc/UI/Component/Shared/RadioButtonChooserView.swift index 72402af6..243d317e 100644 --- a/RIADigiDoc/UI/Component/Shared/RadioButtonChooserView.swift +++ b/RIADigiDoc/UI/Component/Shared/RadioButtonChooserView.swift @@ -62,6 +62,7 @@ struct RadioButtonChooserView: View where T: Hashab accessibilityLabel: accessibilityLabel(option, isSelected(option)), accessibilityInputLabel: accessibilityInputLabel(option) ) + .accessibilityIdentifier(String(describing: option)) Divider() } if trailingSpacer { From 73062ddc28c481c9432e2a322bf09ed21ce7700f Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Wed, 10 Dec 2025 10:42:49 +0200 Subject: [PATCH 7/8] Add accessibility identifier to control code --- RIADigiDoc/UI/Component/Container/Signing/ControlCodeView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RIADigiDoc/UI/Component/Container/Signing/ControlCodeView.swift b/RIADigiDoc/UI/Component/Container/Signing/ControlCodeView.swift index 3de6f1d6..5446f2cc 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/ControlCodeView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/ControlCodeView.swift @@ -50,6 +50,7 @@ struct ControlCodeView: View { .font(typography.displayMedium) .foregroundStyle(theme.onSurface) .scaleEffect(x: Dimensions.Scaling.WideScaling, y: Dimensions.Scaling.DefaultScaling) + .accessibilityIdentifier("controlCode") } } .onDisappear { From 7fb29845c21bb6b10e8ae71ae1436e1ff0b09c04 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 19 Dec 2025 00:59:55 +0200 Subject: [PATCH 8/8] Add proxy to signing methods, fix adding files when container renamed --- .../Sources/CommonsLib/Constants.swift | 1 + .../CommonsLib/DI/CommonsLibContainer.swift | 4 - .../Sources/CommonsLib/Model/ProxyInfo.swift | 2 +- .../Mocks/TestConfigurationProvider.swift | 1 - .../Container/DigiDocContainerWrapper.mm | 9 +- .../Domain/Container/ContainerWrapper.swift | 34 ++++- .../LibdigidocSwift/SignedContainer.swift | 14 ++- .../SignedContainerProtocol.swift | 2 +- .../Container/ContainerWrapperTests.swift | 2 +- .../SignedContainerTests.swift | 85 ++++++++++--- Modules/MobileIdLib/Package.swift | 5 +- .../Service/MobileIdSignService.swift | 53 ++++++-- .../Service/MobileIdSignServiceProtocol.swift | 10 +- .../Service/SmartIdSignService.swift | 51 ++++++-- .../Service/SmartIdSignServiceProtocol.swift | 10 +- .../UtilsLib/Error/FileLastOpenedError.swift | 25 ++++ .../UtilsLib/Extensions/URLExtensions.swift | 48 +++++++ RIADigiDoc.xcodeproj/project.pbxproj | 19 +-- RIADigiDoc/DI/AppContainer.swift | 14 ++- .../Model/Error}/FileOpeningErrors.swift | 0 RIADigiDoc/Domain/Model/FileItem.swift | 2 +- .../AdvancedSettingsRepository.swift | 4 + .../FileOpening/FileOpeningService.swift | 2 +- .../Supporting files/Localizable.xcstrings | 18 +++ .../Container/Crypto/EncryptView.swift | 1 - .../Signing/MobileId/MobileIdView.swift | 18 +-- .../Container/Signing/SigningView.swift | 4 + .../RecentDocumentFileView.swift | 2 +- .../Shared/FloatingLabelTextField.swift | 118 ++++++++++-------- .../Util}/File/FileInspector.swift | 14 +++ .../Util}/File/FileInspectorProtocol.swift | 2 + .../ViewModel/FileOpeningViewModel.swift | 1 + .../Protocols/SigningViewModelProtocol.swift | 1 + .../ViewModel/RecentDocumentsViewModel.swift | 27 ++-- .../Signing/MobileId/MobileIdViewModel.swift | 34 +++-- .../Signing/SmartId/SmartIdViewModel.swift | 38 ++++-- RIADigiDoc/ViewModel/SigningViewModel.swift | 23 +++- ...obileIDSmartIDSettingsViewModelTests.swift | 8 +- .../RecentDocumentsViewModelTests.swift | 43 ++++--- .../MobileId/MobileIdViewModelTests.swift | 5 +- .../SmartId/SmartIdViewModelTests.swift | 5 +- .../ViewModel/SigningViewModelTests.swift | 9 +- codemagic.yaml | 7 +- 43 files changed, 568 insertions(+), 207 deletions(-) create mode 100644 Modules/UtilsLib/Sources/UtilsLib/Error/FileLastOpenedError.swift rename {Modules/CommonsLib/Sources/CommonsLib/Errors => RIADigiDoc/Domain/Model/Error}/FileOpeningErrors.swift (100%) rename {Modules/CommonsLib/Sources/CommonsLib => RIADigiDoc/Util}/File/FileInspector.swift (72%) rename {Modules/CommonsLib/Sources/CommonsLib => RIADigiDoc/Util}/File/FileInspectorProtocol.swift (89%) diff --git a/Modules/CommonsLib/Sources/CommonsLib/Constants.swift b/Modules/CommonsLib/Sources/CommonsLib/Constants.swift index 95990ce5..4141ac66 100644 --- a/Modules/CommonsLib/Sources/CommonsLib/Constants.swift +++ b/Modules/CommonsLib/Sources/CommonsLib/Constants.swift @@ -81,6 +81,7 @@ public struct Constants { public struct Identifier { public static let Group = "group.ee.ria.digidoc.ios" public static let GroupDownload = "group.ee.ria.digidoc.ios.download" + public static let GroupLastOpenedAttribute = "group.ee.ria.digidoc.ios.lastOpened" } public struct Folder { diff --git a/Modules/CommonsLib/Sources/CommonsLib/DI/CommonsLibContainer.swift b/Modules/CommonsLib/Sources/CommonsLib/DI/CommonsLibContainer.swift index c985c4bd..fbd19696 100644 --- a/Modules/CommonsLib/Sources/CommonsLib/DI/CommonsLibContainer.swift +++ b/Modules/CommonsLib/Sources/CommonsLib/DI/CommonsLibContainer.swift @@ -27,10 +27,6 @@ public extension Container { .singleton } - var fileInspector: Factory { - self { FileInspector() } - } - var urlResourceChecker: Factory { self { URLResourceChecker() } } diff --git a/Modules/CommonsLib/Sources/CommonsLib/Model/ProxyInfo.swift b/Modules/CommonsLib/Sources/CommonsLib/Model/ProxyInfo.swift index 77f16e46..31e512f9 100644 --- a/Modules/CommonsLib/Sources/CommonsLib/Model/ProxyInfo.swift +++ b/Modules/CommonsLib/Sources/CommonsLib/Model/ProxyInfo.swift @@ -17,7 +17,7 @@ * */ -public struct ProxyInfo: Sendable { +public struct ProxyInfo: Sendable, Equatable { public var option: ProxySettingsOption public var host: String public var port: Int diff --git a/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift b/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift index b57f5d5f..89488329 100644 --- a/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift +++ b/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift @@ -35,7 +35,6 @@ public class TestConfigurationProvider { tslUrl: String = "https://tsl.someUrl.abc", tslCerts: [String] = ["cert1", "cert2"], tsaUrl: String = "https://tsa.someUrl.abc", - ocspIssuers: [String: String] = ["url1": "issuer1"], ldapPersonUrls: [String] = [ "https://ldap-person.someUrl.abc", "https://ldap-person.someUrl2.abc" diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm index c0112bda..c1979bf5 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm @@ -269,7 +269,10 @@ + (void)addDataFilesToContainerWithPath:(NSString *)containerPath std::vector causes = e.causes(); NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: [NSString stringWithUTF8String:e.msg().c_str()], - @"causes": [ExceptionUtil exceptionCauses:static_cast(&causes)] + @"causes": @{ + @"exceptions": [ExceptionUtil exceptionCauses:static_cast(&causes)], + @"fileName": dataFilePath.lastPathComponent + } }; NSError *addFileError = [NSError errorWithDomain:@"LibdigidocLib" code:e.code() userInfo:userInfo]; @@ -289,7 +292,9 @@ + (void)addDataFilesToContainerWithPath:(NSString *)containerPath NSLocalizedDescriptionKey: summary, @"failedFileCount": @(failedCount), @"totalFileCount": @(totalFileCount), - @"causes": errors + @"causes": @{ + @"errors": errors + } }; NSError *combined = [NSError errorWithDomain:@"LibdigidocLib" code:1 userInfo:info]; completion(combined); diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift index 64bf491c..e68f17af 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift @@ -151,12 +151,36 @@ public actor ContainerWrapper: ContainerWrapperProtocol { return try await open(containerFile: containerFile, isSivaConfirmed: true) } catch { - let nsError = (error as NSError?) ?? NSError(domain: "ContainerWrapper - cannot add data files", code: 4) - throw DigiDocError.addingFilesToContainerFailed( - ErrorDetail( - nsError: nsError + ContainerWrapper.logger.error("Unable to add data files. \(error)") + + let nsError = error as NSError + + let errors = (nsError.userInfo["causes"] as? [String: Any])?["errors"] as? [NSError] ?? [] + + let duplicatePrefix = "Document with same file name" + + let duplicateCount = errors.filter { $0.localizedDescription.hasPrefix(duplicatePrefix) }.count + let totalCount = errors.count + + if duplicateCount == totalCount && totalCount > 1 { + throw DigiDocError.addingFilesToContainerFailed( + ErrorDetail(message: "Multiple documents already exist", code: 4, userInfo: [ + "totalFileCount": String(totalCount), + "duplicateFileCount": String(duplicateCount) + ]) ) - ) + } else { + let nsError = (error as NSError?) ?? NSError( + domain: "ContainerWrapper - cannot add data files", + code: 5 + ) + + throw DigiDocError.addingFilesToContainerFailed( + ErrorDetail( + nsError: nsError + ) + ) + } } } diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift index 56e4e9b9..762241b5 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift @@ -149,7 +149,7 @@ public actor SignedContainer: SignedContainerProtocol { } @discardableResult - public func renameContainer(to newName: String) async throws -> URL { + public func renameContainer(to newName: String) async throws -> SignedContainerProtocol { let fileName = newName.isEmpty ? CommonsLib.Constants.Container.DefaultName : newName @@ -186,9 +186,17 @@ public actor SignedContainer: SignedContainerProtocol { try fileManager.moveItem(at: currentURL, to: uniqueFileURL) - containerFile = uniqueFileURL + let container = try await ContainerWrapper( + fileManager: fileManager + ).open(containerFile: uniqueFileURL, isSivaConfirmed: true) - return uniqueFileURL + return SignedContainer( + containerFile: uniqueFileURL, + isExistingContainer: true, + container: container, + fileManager: Container.shared.fileManager(), + containerUtil: Container.shared.containerUtil() + ) } public func saveDataFile(dataFile: DataFileWrapper, to directory: URL?) async throws -> URL { diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift index 0fb21e79..64e626f9 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift @@ -28,7 +28,7 @@ public protocol SignedContainerProtocol: GeneralContainer, Sendable { func getContainerName() async -> String func getContainerMimetype() async -> String func getRawContainerFile() async -> URL? - @discardableResult func renameContainer(to newName: String) async throws -> URL + @discardableResult func renameContainer(to newName: String) async throws -> SignedContainerProtocol @discardableResult func addDataFiles( _ dataFiles: [URL], to containerFile: URL diff --git a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Container/ContainerWrapperTests.swift b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Container/ContainerWrapperTests.swift index ec1a844e..5071150a 100644 --- a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Container/ContainerWrapperTests.swift +++ b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/Container/ContainerWrapperTests.swift @@ -180,7 +180,7 @@ struct ContainerWrapperTests { try? FileManager.default.removeItem(at: containerFile) } - let expectedErrorMessage = "Could not add files" + let expectedErrorMessage = "Multiple documents already exist" do { try await containerWrapper.addDataFiles(containerFile: containerFile, dataFiles: dataFileURLs) diff --git a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift index cb6849e5..69716fa0 100644 --- a/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift +++ b/Modules/LibdigidocLib/Tests/LibdigidocLibTests/LibdigidocSwift/SignedContainerTests.swift @@ -164,30 +164,57 @@ final class SignedContainerTests { @Test func renameContainer_success() async throws { - let originalURL = URL(fileURLWithPath: "/tmp/original.asice") + let exampleContainer = try #require( TestFileUtil.pathForResourceFile(fileName: "example", ext: "asice")) let newFileName = "renamed.asice" - let directoryURL = originalURL.deletingLastPathComponent() - let uniqueFileURL = directoryURL.appending(path: "renamed_unique.asice") + let tempDirectoryURL = TestFileUtil.getTemporaryDirectory( + subfolder: "SignedContainerTests" + ) + let uniqueFileURL = tempDirectoryURL.appending(path: "renamed_unique.asice") + + let localExampleContainer = tempDirectoryURL.appending(path: + "\(UUID().uuidString)-\(exampleContainer.lastPathComponent)" + ) + + try FileManager.default.copyItem( + at: exampleContainer, + to: localExampleContainer + ) + + defer { + try? FileManager.default.removeItem(at: localExampleContainer) + try? FileManager.default.removeItem(at: tempDirectoryURL) + } mockContainerUtil.getContainerFileHandler = { _, _ in uniqueFileURL } mockContainerWrapper.saveDataFileHandler = { _, _, _ in uniqueFileURL } + mockFileManager.moveItemHandler = { _, _ in + do { + try FileManager.default.moveItem(at: localExampleContainer, to: uniqueFileURL) + } catch { + Issue.record("Expected moveItem to succeed") + return + } + } + let container = SignedContainer( - containerFile: originalURL, + containerFile: localExampleContainer, isExistingContainer: true, container: mockContainerWrapper, fileManager: mockFileManager, containerUtil: mockContainerUtil ) - let result = try await container.renameContainer(to: newFileName) + let renamedContainer = try await container.renameContainer(to: newFileName) #expect(mockFileManager.moveItemCallCount == 1) - #expect(mockFileManager.moveItemArgValues.first?.srcURL == originalURL) + #expect(mockFileManager.moveItemArgValues.first?.srcURL == localExampleContainer) #expect(mockFileManager.moveItemArgValues.first?.dstURL == uniqueFileURL) - #expect(result == uniqueFileURL) + let containerUrl = await renamedContainer.getRawContainerFile() + + #expect(containerUrl == uniqueFileURL) } @Test @@ -220,11 +247,27 @@ final class SignedContainerTests { @Test func renameContainer_returnURLWithDefaultNameWhenEmptyNewFileName() async throws { - let originalURL = URL(fileURLWithPath: "/tmp/original.asice") + let exampleContainer = try #require( TestFileUtil.pathForResourceFile(fileName: "example", ext: "asice")) let emptyNewName = "" - let directoryURL = originalURL.deletingLastPathComponent() + let tempDirectoryURL = TestFileUtil.getTemporaryDirectory( + subfolder: "SignedContainerTests" + ) let defaultFileName = CommonsLib.Constants.Container.DefaultName - let uniqueFileURL = directoryURL.appending(path: "\(defaultFileName)_unique.asice") + let uniqueFileURL = tempDirectoryURL.appending(path: "\(defaultFileName)_unique.asice") + + let localExampleContainer = tempDirectoryURL.appending(path: + "\(UUID().uuidString)-\(exampleContainer.lastPathComponent)" + ) + + try FileManager.default.copyItem( + at: exampleContainer, + to: localExampleContainer + ) + + defer { + try? FileManager.default.removeItem(at: localExampleContainer) + try? FileManager.default.removeItem(at: tempDirectoryURL) + } mockContainerUtil.getContainerFileHandler = { url, _ in #expect(url.lastPathComponent.starts(with: defaultFileName)) @@ -233,20 +276,32 @@ final class SignedContainerTests { mockContainerWrapper.saveDataFileHandler = { _, _, _ in uniqueFileURL } - let container = SignedContainer( - containerFile: originalURL, + mockFileManager.moveItemHandler = { _, _ in + do { + try FileManager.default.moveItem(at: localExampleContainer, to: uniqueFileURL) + } catch { + Issue.record("Expected moveItem to succeed") + return + } + } + + let containerToRename = SignedContainer( + containerFile: localExampleContainer, isExistingContainer: false, container: mockContainerWrapper, fileManager: mockFileManager, containerUtil: mockContainerUtil ) - let resultURL = try await container.renameContainer(to: emptyNewName) + let renamedContainer = try await containerToRename.renameContainer(to: emptyNewName) #expect(mockFileManager.moveItemCallCount == 1) - #expect(mockFileManager.moveItemArgValues.first?.srcURL == originalURL) + #expect(mockFileManager.moveItemArgValues.first?.srcURL == localExampleContainer) #expect(mockFileManager.moveItemArgValues.first?.dstURL == uniqueFileURL) - #expect(resultURL == uniqueFileURL) + + let containerUrl = await renamedContainer.getRawContainerFile() + + #expect(containerUrl == uniqueFileURL) } @Test diff --git a/Modules/MobileIdLib/Package.swift b/Modules/MobileIdLib/Package.swift index 25e80d60..1afe1c2a 100644 --- a/Modules/MobileIdLib/Package.swift +++ b/Modules/MobileIdLib/Package.swift @@ -16,7 +16,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/hmlongco/Factory", exact: .init(2, 5, 3)), .package(url: "https://github.com/Alamofire/Alamofire.git", exact: .init(5, 10, 2)), - .package(path: "../CommonsLib") + .package(path: "../CommonsLib"), + .package(path: "../UtilsLib") ], targets: [ .target( @@ -24,6 +25,7 @@ let package = Package( dependencies: [ "Alamofire", "CommonsLib", + "UtilsLib", .product(name: "FactoryKit", package: "Factory") ], swiftSettings: [ @@ -43,6 +45,7 @@ let package = Package( dependencies: [ "MobileIdLib", "CommonsLib", + "UtilsLib", .product(name: "CommonsLibMocks", package: "commonslib"), .product(name: "FactoryTesting", package: "Factory") ] diff --git a/Modules/MobileIdLib/Sources/MobileIdLib/Service/MobileIdSignService.swift b/Modules/MobileIdLib/Sources/MobileIdLib/Service/MobileIdSignService.swift index ad031141..c3832612 100644 --- a/Modules/MobileIdLib/Sources/MobileIdLib/Service/MobileIdSignService.swift +++ b/Modules/MobileIdLib/Sources/MobileIdLib/Service/MobileIdSignService.swift @@ -21,11 +21,13 @@ import Foundation import OSLog import Alamofire import CommonsLib +import UtilsLib actor MobileIdSignService: MobileIdSignServiceProtocol { private static let logger = Logger(subsystem: "ee.ria.digidoc.RIADigiDoc", category: "MobileIdSignService") private var session: Session? + private var currentProxy: ProxyInfo? // swiftlint:disable:next function_parameter_count public func getCertificateRequest( @@ -34,7 +36,8 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { relyingPartyUUID: String, phoneNumber: String, nationalIdentityNumber: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> MobileIdCertificateResponse { let request = MobileIdCertificateRequest( relyingPartyName: relyingPartyName, @@ -47,7 +50,8 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { url: url, method: .post, parameters: request, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } @@ -63,7 +67,8 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { language: String, displayText: String, displayTextFormat: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> MobileIdSignatureResponse { let request = MobileIdSignatureRequest( certificateRequest: .init( @@ -83,7 +88,8 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { url: url, method: .post, parameters: request, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } @@ -91,7 +97,8 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { url: String, sessionId: String, pollingTimeout: Int, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> MobileIdSessionResponse { let pollingTimeoutMs = pollingTimeout * 1000 @@ -100,7 +107,8 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { url: "\(url)/\(sessionId)", method: .get, parameters: ["timeoutMs": pollingTimeoutMs], - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) if let response = sessionResponse, @@ -122,14 +130,19 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { url: String, method: HTTPMethod, parameters: P? = nil, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> T { let encoder: ParameterEncoder = method == .get ? URLEncodedFormParameterEncoder.default : JSONParameterEncoder.default do { - let session = try await ensureSession(url: url, trustedCertificates: trustedCertificates) + let session = try await ensureSession( + url: url, + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo + ) let headers = MobileIdSignService.defaultHeaders() let response = await session.request( @@ -161,8 +174,16 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { } } - private func ensureSession(url: String, trustedCertificates: [SecCertificate]) async throws -> Session { - if let existing = session { return existing } + private func ensureSession( + url: String, + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo + ) async throws -> Session { + if currentProxy == proxyInfo { + if let existing = session { return existing } + } + + currentProxy = proxyInfo guard let host = URL(string: url)?.host else { MobileIdSignService.logger.error( @@ -173,7 +194,8 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { let newSession = MobileIdSignService.createAlamofireSession( host: host, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) session = newSession return newSession @@ -187,7 +209,11 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { ] } - private static func createAlamofireSession(host: String, trustedCertificates: [SecCertificate]) -> Session { + private static func createAlamofireSession( + host: String, + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo + ) -> Session { let evaluators = [host: PinnedCertificatesTrustEvaluator(certificates: trustedCertificates)] let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = TimeInterval(Constants.Signing.Timeout) @@ -195,7 +221,8 @@ actor MobileIdSignService: MobileIdSignServiceProtocol { config.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData config.urlCache = nil - return Session( + return Session.withProxy( + proxyInfo: proxyInfo, configuration: config, serverTrustManager: ServerTrustManager(evaluators: evaluators) ) diff --git a/Modules/MobileIdLib/Sources/MobileIdLib/Service/MobileIdSignServiceProtocol.swift b/Modules/MobileIdLib/Sources/MobileIdLib/Service/MobileIdSignServiceProtocol.swift index 2792e739..87392783 100644 --- a/Modules/MobileIdLib/Sources/MobileIdLib/Service/MobileIdSignServiceProtocol.swift +++ b/Modules/MobileIdLib/Sources/MobileIdLib/Service/MobileIdSignServiceProtocol.swift @@ -18,6 +18,7 @@ */ import Foundation +import CommonsLib // swiftlint:disable function_parameter_count /// @mockable @@ -29,7 +30,8 @@ public protocol MobileIdSignServiceProtocol: Sendable { relyingPartyUUID: String, phoneNumber: String, nationalIdentityNumber: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> MobileIdCertificateResponse func getSignatureRequest( @@ -43,14 +45,16 @@ public protocol MobileIdSignServiceProtocol: Sendable { language: String, displayText: String, displayTextFormat: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> MobileIdSignatureResponse func getSessionRequest( url: String, sessionId: String, pollingTimeout: Int, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> MobileIdSessionResponse func getVerificationCode(hash: Data) async -> String? diff --git a/Modules/SmartIdLib/Sources/SmartIdLib/Service/SmartIdSignService.swift b/Modules/SmartIdLib/Sources/SmartIdLib/Service/SmartIdSignService.swift index 3c811f34..552a366c 100644 --- a/Modules/SmartIdLib/Sources/SmartIdLib/Service/SmartIdSignService.swift +++ b/Modules/SmartIdLib/Sources/SmartIdLib/Service/SmartIdSignService.swift @@ -31,6 +31,7 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { ) private var session: Session? + private var currentProxy: ProxyInfo? // swiftlint:disable:next function_parameter_count public func getCertificateRequest( @@ -39,7 +40,8 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { relyingPartyUUID: String, country: String, nationalIdentityNumber: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> SmartIdSessionIdResponse { let request = SmartIdCertificateRequest( relyingPartyName: relyingPartyName, @@ -52,7 +54,8 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { url: "\(url)/\(semanticsIdentifier)", method: .post, parameters: request, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } @@ -66,7 +69,8 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { hashType: String, allowedInteractionsOrderType: String, displayText200: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> SmartIdSessionIdResponse { let request = SmartIdSignatureRequest( relyingPartyName: relyingPartyName, @@ -83,7 +87,8 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { url: "\(url)/\(documentNumber)", method: .post, parameters: request, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } @@ -92,6 +97,7 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { sessionId: String, pollingTimeout: Int, trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> SmartIdSessionResponse { let pollingTimeoutMs = pollingTimeout * 1000 @@ -100,7 +106,8 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { url: "\(url)/\(sessionId)", method: .get, parameters: ["timeoutMs": pollingTimeoutMs], - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) if let response = sessionResponse, @@ -121,7 +128,8 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { url: String, method: HTTPMethod, parameters: P? = nil, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> T { return try await withCheckedThrowingContinuation { continuation in Task { @@ -130,7 +138,11 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { URLEncodedFormParameterEncoder.default : JSONParameterEncoder.default - let session = try await ensureSession(url: url, trustedCertificates: trustedCertificates) + let session = try await ensureSession( + url: url, + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo + ) let headers = SmartIdSignService.defaultHeaders() let response = await session.request( @@ -193,8 +205,16 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { ] } - private func ensureSession(url: String, trustedCertificates: [SecCertificate]) async throws -> Session { - if let existing = session { return existing } + private func ensureSession( + url: String, + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo + ) async throws -> Session { + if currentProxy == proxyInfo { + if let existing = session { return existing } + } + + currentProxy = proxyInfo guard let host = URL(string: url)?.host else { SmartIdSignService.logger.error( @@ -205,13 +225,19 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { let newSession = SmartIdSignService.createAlamofireSession( host: host, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) + session = newSession return newSession } - private static func createAlamofireSession(host: String, trustedCertificates: [SecCertificate]) -> Session { + private static func createAlamofireSession( + host: String, + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo + ) -> Session { let evaluators = [host: PinnedCertificatesTrustEvaluator(certificates: trustedCertificates)] let config = URLSessionConfiguration.default @@ -220,7 +246,8 @@ public actor SmartIdSignService: SmartIdSignServiceProtocol { config.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData config.urlCache = nil - return Session( + return Session.withProxy( + proxyInfo: proxyInfo, configuration: config, serverTrustManager: ServerTrustManager(evaluators: evaluators) ) diff --git a/Modules/SmartIdLib/Sources/SmartIdLib/Service/SmartIdSignServiceProtocol.swift b/Modules/SmartIdLib/Sources/SmartIdLib/Service/SmartIdSignServiceProtocol.swift index 70705c84..e01570cd 100644 --- a/Modules/SmartIdLib/Sources/SmartIdLib/Service/SmartIdSignServiceProtocol.swift +++ b/Modules/SmartIdLib/Sources/SmartIdLib/Service/SmartIdSignServiceProtocol.swift @@ -18,6 +18,7 @@ */ import Foundation +import CommonsLib // swiftlint:disable function_parameter_count /// @mockable @@ -29,7 +30,8 @@ public protocol SmartIdSignServiceProtocol: Sendable { relyingPartyUUID: String, country: String, nationalIdentityNumber: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> SmartIdSessionIdResponse func getSignatureRequest( @@ -41,14 +43,16 @@ public protocol SmartIdSignServiceProtocol: Sendable { hashType: String, allowedInteractionsOrderType: String, displayText200: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> SmartIdSessionIdResponse func getSessionRequest( url: String, sessionId: String, pollingTimeout: Int, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> SmartIdSessionResponse func getVerificationCode(digest: Data) async -> String diff --git a/Modules/UtilsLib/Sources/UtilsLib/Error/FileLastOpenedError.swift b/Modules/UtilsLib/Sources/UtilsLib/Error/FileLastOpenedError.swift new file mode 100644 index 00000000..bba49702 --- /dev/null +++ b/Modules/UtilsLib/Sources/UtilsLib/Error/FileLastOpenedError.swift @@ -0,0 +1,25 @@ +/* + * 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 + * + */ + +enum FileLastOpenedError: Error { + case writeFailed(errno: Int32) + case readFailed(errno: Int32) + case removeFailed(errno: Int32) + case invalidData +} diff --git a/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift b/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift index d53d9442..30301831 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift @@ -321,3 +321,51 @@ extension URL { return preferredMimeType.lowercased() } } + +extension URL { + + private var lastOpenedXattrName: String { Constants.Identifier.GroupLastOpenedAttribute } + + public func markAsOpened() throws { + var timestamp = Date().timeIntervalSince1970 + let data = Data(bytes: ×tamp, count: MemoryLayout.size(ofValue: timestamp)) + + let result = data.withUnsafeBytes { ptr in + setxattr( + self.path, + lastOpenedXattrName, + ptr.baseAddress, + data.count, + 0, + 0 + ) + } + guard result == 0 else { + throw FileLastOpenedError.writeFailed(errno: errno) + } + } + + public func lastOpened() throws -> Date? { + var buffer = [UInt8](repeating: 0, count: MemoryLayout.size) + let size = getxattr(self.path, lastOpenedXattrName, &buffer, buffer.count, 0, 0) + + guard size != -1 else { + if errno == ENOATTR { return nil } + throw FileLastOpenedError.readFailed(errno: errno) + } + guard size == buffer.count else { throw FileLastOpenedError.invalidData } + + let interval = buffer.withUnsafeBytes { $0.load(as: Double.self) } + return Date(timeIntervalSince1970: interval) + } + + public func removeLastOpenedXattr() throws { + let result = removexattr(self.path, lastOpenedXattrName, 0) + + if result != 0 { + if errno == ENOATTR { return } else { + throw FileLastOpenedError.removeFailed(errno: errno) + } + } + } +} diff --git a/RIADigiDoc.xcodeproj/project.pbxproj b/RIADigiDoc.xcodeproj/project.pbxproj index 050123c5..4636a5f4 100644 --- a/RIADigiDoc.xcodeproj/project.pbxproj +++ b/RIADigiDoc.xcodeproj/project.pbxproj @@ -144,6 +144,7 @@ Domain/Model/EncryptionServerOption.swift, Domain/Model/EncryptionServerOptionId.swift, Domain/Model/Error/ErrorMessage.swift, + Domain/Model/Error/FileOpeningErrors.swift, Domain/Model/FileItem.swift, Domain/Model/KeychainKey.swift, Domain/Model/Notification/ContainerNotificationType.swift, @@ -178,6 +179,8 @@ UI/Theme/Theme.swift, Util/Certificate/CertificateUtil.swift, Util/Certificate/CertificateUtilProtocol.swift, + Util/File/FileInspector.swift, + Util/File/FileInspectorProtocol.swift, Util/Language/LanguageSettings.swift, Util/Language/LanguageSettingsProtocol.swift, Util/Proxy/ProxyUtil.swift, @@ -890,9 +893,9 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = Extensions/FileImportShareExtension/FileImportShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = ET847QJV9F; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Extensions/FileImportShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Import to RIA DigiDoc"; @@ -921,9 +924,9 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = Extensions/FileImportShareExtension/FileImportShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = ET847QJV9F; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Extensions/FileImportShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Import to RIA DigiDoc"; @@ -1138,11 +1141,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = RIADigiDoc/RIADigiDoc.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"RIADigiDoc/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = ET847QJV9F; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -1194,11 +1197,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = RIADigiDoc/RIADigiDoc.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"RIADigiDoc/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = ET847QJV9F; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; diff --git a/RIADigiDoc/DI/AppContainer.swift b/RIADigiDoc/DI/AppContainer.swift index e18873d0..94b8f2bc 100644 --- a/RIADigiDoc/DI/AppContainer.swift +++ b/RIADigiDoc/DI/AppContainer.swift @@ -222,7 +222,8 @@ extension Container { @MainActor in RecentDocumentsViewModel( sharedContainerViewModel: self.sharedContainerViewModel(), - fileManager: self.fileManager() + fileManager: self.fileManager(), + fileInspector: self.fileInspector() ) } } @@ -270,6 +271,11 @@ extension Container { self { @MainActor in CertificateDetailViewModel() } } + @MainActor + var fileInspector: Factory { + self { @MainActor in FileInspector() } + } + @MainActor var advancedSettingsRepository: Factory { self { @MainActor in @@ -384,7 +390,8 @@ extension Container { configurationRepository: self.configurationRepository(), mobileIdSignService: self.mobileIdSignService(), certificateUtil: self.certificateUtil(), - dataStore: self.dataStore() + dataStore: self.dataStore(), + proxyUtil: self.proxyUtil() ) } } @@ -397,7 +404,8 @@ extension Container { smartIdSignService: self.smartIdSignService(), certificateUtil: self.certificateUtil(), notificationUtil: self.notificationUtil(), - dataStore: self.dataStore() + dataStore: self.dataStore(), + proxyUtil: self.proxyUtil() ) } } diff --git a/Modules/CommonsLib/Sources/CommonsLib/Errors/FileOpeningErrors.swift b/RIADigiDoc/Domain/Model/Error/FileOpeningErrors.swift similarity index 100% rename from Modules/CommonsLib/Sources/CommonsLib/Errors/FileOpeningErrors.swift rename to RIADigiDoc/Domain/Model/Error/FileOpeningErrors.swift diff --git a/RIADigiDoc/Domain/Model/FileItem.swift b/RIADigiDoc/Domain/Model/FileItem.swift index ae3e7e5f..c05f8a15 100644 --- a/RIADigiDoc/Domain/Model/FileItem.swift +++ b/RIADigiDoc/Domain/Model/FileItem.swift @@ -23,5 +23,5 @@ public struct FileItem: Identifiable { public let id = UUID() public let name: String public let url: URL - public let modifiedDate: Date + public let lastOpened: Date } diff --git a/RIADigiDoc/Domain/Repository/AdvancedSettings/AdvancedSettingsRepository.swift b/RIADigiDoc/Domain/Repository/AdvancedSettings/AdvancedSettingsRepository.swift index 2ae29774..b93ac6b4 100644 --- a/RIADigiDoc/Domain/Repository/AdvancedSettings/AdvancedSettingsRepository.swift +++ b/RIADigiDoc/Domain/Repository/AdvancedSettings/AdvancedSettingsRepository.swift @@ -108,8 +108,12 @@ actor AdvancedSettingsRepository: AdvancedSettingsRepositoryProtocol { "\(certificateBaseName).\(extensionToUse)" ) + _ = url.startAccessingSecurityScopedResource() + try fileManager.copyItem(at: url, to: destinationURL) + url.stopAccessingSecurityScopedResource() + return try await getCertificateContent(certFileURL: destinationURL) } catch { await AdvancedSettingsRepository.logger.error("Unable to import certificate: \(error)") diff --git a/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift b/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift index eb701451..df9a9c28 100644 --- a/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift +++ b/RIADigiDoc/Domain/Service/FileOpening/FileOpeningService.swift @@ -41,7 +41,7 @@ actor FileOpeningService: FileOpeningServiceProtocol { } func isFileSizeValid(url: URL) async throws -> Bool { - return try fileInspector.fileSize(for: url) > 0 + return try await fileInspector.fileSize(for: url) > 0 } func getValidFiles( diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index 201bc165..2e374187 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -4207,6 +4207,24 @@ } } }, + "Multiple documents already exist" : { + "comment" : "When trying to add multiple data files that already exist in the container", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ files already exist in the container" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ faili on juba ümbrikus olemas" + } + } + } + }, "Next" : { "comment" : "Encryption view bottom bar action", "extractionState" : "manual", diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index 8dcdd1d1..a5179671 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -358,7 +358,6 @@ struct EncryptView: View { rightButtonAccessibilityLabel: rightButtonLabel.lowercased(), rightButtonAction: { if viewModel.isContainerWithoutRecipients { - pathManager.removeLast() pathManager.navigate(to: .encryptRecipientView ) diff --git a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift index b8e68607..de48a7f4 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift @@ -119,6 +119,15 @@ struct MobileIdView: View { } } } + .onChange(of: viewModel.mobileIdMessageKey, { _, newValue in + if let messageKey = newValue, + !messageKey.isEmpty { + let extraArguments = viewModel.mobileIdAlertMessageExtraArguments + Toast.show( + languageSettings.localized(messageKey, extraArguments) + ) + } + }) .fullScreenCover(isPresented: $showRoleView) { RoleView( onComplete: { roles, city, state, country, zipCode in @@ -180,18 +189,11 @@ struct MobileIdView: View { guard let container = updatedContainer else { cancelSigning() isSigning = false - if let messageKey = viewModel.mobileIdMessageKey, - !messageKey.isEmpty { - let extraArguments = viewModel.mobileIdAlertMessageExtraArguments - Toast.show( - languageSettings.localized(messageKey, extraArguments) - ) - } - return } cancelSigning() + isSigning = false onSuccess(container) dismiss() diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index a1d08e11..383f47b4 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -202,6 +202,10 @@ struct SigningView: View { containerURL: viewModel.containerURL ) + if let containerURL = tempContainerURL { + viewModel.removeLastOpenedXattr(from: containerURL) + } + if fileUtil.fileExists(fileLocation: tempContainerURL) { viewModel.isShowingContainerFileSaver = true } diff --git a/RIADigiDoc/UI/Component/Recent documents/RecentDocumentFileView.swift b/RIADigiDoc/UI/Component/Recent documents/RecentDocumentFileView.swift index 8fa18d6c..ff2dc311 100644 --- a/RIADigiDoc/UI/Component/Recent documents/RecentDocumentFileView.swift +++ b/RIADigiDoc/UI/Component/Recent documents/RecentDocumentFileView.swift @@ -82,7 +82,7 @@ struct RecentDocumentFileView: View { file: FileItem( name: "test.asice", url: URL(fileURLWithPath: "/path/test.asice"), - modifiedDate: Date.now + lastOpened: Date.now ), fileIndex: 0, onOpenContainer: {}, diff --git a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift index ac11fc0c..2235971f 100644 --- a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift +++ b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift @@ -255,73 +255,83 @@ struct FloatingLabelTextField: View { private var inputField: some View { Group { if isSecure && !isPasswordVisible { - SecureField(placeholder, text: $text) - .multilineTextAlignment(.leading) - .disabled(isDisabled) - .keyboardType(keyboardType) - .submitLabel(submitLabel) - .accessibilityFocused($isAccessibilityFocused) - .onSubmit { - isFocused = false - isAccessibilityFocused = true - } - .toolbar { - ToolbarItem(placement: .keyboard) { - if fieldIsFocused { - HStack { - if showDashButton { - Button( - action: { text.append("-") }, - label: { Text(verbatim: "-") } - ) - } - + SecureField( + placeholder, + text: $text, + prompt: Text(verbatim: placeholder) + .foregroundStyle(theme.onSurfaceVariant) + ) + .multilineTextAlignment(.leading) + .disabled(isDisabled) + .keyboardType(keyboardType) + .submitLabel(submitLabel) + .accessibilityFocused($isAccessibilityFocused) + .onSubmit { + isFocused = false + isAccessibilityFocused = true + } + .toolbar { + ToolbarItem(placement: .keyboard) { + if fieldIsFocused { + HStack { + if showDashButton { Button( - action: { - fieldIsFocused = false - isAccessibilityFocused = true - }, - label: { Text(verbatim: languageSettings.localized("Done")) } + action: { text.append("-") }, + label: { Text(verbatim: "-") } ) } + + Button( + action: { + fieldIsFocused = false + isAccessibilityFocused = true + }, + label: { Text(verbatim: languageSettings.localized("Done")) } + ) } } } - .accessibilityValue(Text(verbatim: "")) + } + .accessibilityValue(Text(verbatim: "")) } else { - TextField(placeholder, text: $text) - .multilineTextAlignment(.leading) - .disabled(isDisabled) - .keyboardType(keyboardType) - .submitLabel(submitLabel) - .accessibilityFocused($isAccessibilityFocused) - .onSubmit { - fieldIsFocused = false - isAccessibilityFocused = true - } - .toolbar { - ToolbarItem(placement: .keyboard) { - if fieldIsFocused { - HStack { - if showDashButton { - Button( - action: { text.append("-") }, - label: { Text(verbatim: "-") } - ) - } - + TextField( + placeholder, + text: $text, + prompt: Text(verbatim: placeholder) + .foregroundStyle(theme.onSurfaceVariant) + ) + .multilineTextAlignment(.leading) + .disabled(isDisabled) + .keyboardType(keyboardType) + .submitLabel(submitLabel) + .accessibilityFocused($isAccessibilityFocused) + .onSubmit { + fieldIsFocused = false + isAccessibilityFocused = true + } + .toolbar { + ToolbarItem(placement: .keyboard) { + if fieldIsFocused { + HStack { + if showDashButton { Button( - action: { - fieldIsFocused = false - isAccessibilityFocused = true - }, - label: { Text(verbatim: languageSettings.localized("Done")) } + action: { text.append("-") }, + label: { Text(verbatim: "-") } ) } + + Button( + action: { + fieldIsFocused = false + isAccessibilityFocused = true + }, + label: { Text(verbatim: languageSettings.localized("Done")) } + ) } } } - .accessibilityValue(Text(verbatim: "")) + } + .accessibilityValue(Text(verbatim: "")) } } .font(typography.bodyLarge) diff --git a/Modules/CommonsLib/Sources/CommonsLib/File/FileInspector.swift b/RIADigiDoc/Util/File/FileInspector.swift similarity index 72% rename from Modules/CommonsLib/Sources/CommonsLib/File/FileInspector.swift rename to RIADigiDoc/Util/File/FileInspector.swift index 8d7988fb..bfd4d846 100644 --- a/Modules/CommonsLib/Sources/CommonsLib/File/FileInspector.swift +++ b/RIADigiDoc/Util/File/FileInspector.swift @@ -32,4 +32,18 @@ public struct FileInspector: FileInspectorProtocol { return fileSize } + + public func contentAccessDate(for url: URL) throws -> Date { + let contentAccessDateResource = try url.resourceValues(forKeys: [.contentAccessDateKey]) + + guard let contentAccessDate = contentAccessDateResource.contentAccessDate else { + return Date() + } + + return contentAccessDate + } + + public func lastOpened(for url: URL) throws -> Date { + try url.lastOpened() ?? contentAccessDate(for: url) + } } diff --git a/Modules/CommonsLib/Sources/CommonsLib/File/FileInspectorProtocol.swift b/RIADigiDoc/Util/File/FileInspectorProtocol.swift similarity index 89% rename from Modules/CommonsLib/Sources/CommonsLib/File/FileInspectorProtocol.swift rename to RIADigiDoc/Util/File/FileInspectorProtocol.swift index e6949c24..fc2ec402 100644 --- a/Modules/CommonsLib/Sources/CommonsLib/File/FileInspectorProtocol.swift +++ b/RIADigiDoc/Util/File/FileInspectorProtocol.swift @@ -22,4 +22,6 @@ import Foundation /// @mockable public protocol FileInspectorProtocol: Sendable { func fileSize(for url: URL) throws -> Int + func contentAccessDate(for url: URL) throws -> Date + func lastOpened(for url: URL) throws -> Date } diff --git a/RIADigiDoc/ViewModel/FileOpeningViewModel.swift b/RIADigiDoc/ViewModel/FileOpeningViewModel.swift index bd3a7efd..aafb2564 100644 --- a/RIADigiDoc/ViewModel/FileOpeningViewModel.swift +++ b/RIADigiDoc/ViewModel/FileOpeningViewModel.swift @@ -96,6 +96,7 @@ class FileOpeningViewModel: FileOpeningViewModelProtocol { sharedContainerViewModel.setSignedContainer(container) } + try await container.getRawContainerFile()?.markAsOpened() handleLoadingSuccess(isSivaConfirmed: true) } catch { FileOpeningViewModel.logger.error("Unable to handle SiVa container. \(error)") diff --git a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift index f31300b2..578b495e 100644 --- a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift @@ -41,4 +41,5 @@ public protocol SigningViewModelProtocol: Sendable { func removeSignature(_ signature: SignatureWrapper) async func removeDataFile(_ dataFile: DataFileWrapper) async func isSignatureAdded() -> Bool + func removeLastOpenedXattr(from url: URL) } diff --git a/RIADigiDoc/ViewModel/RecentDocumentsViewModel.swift b/RIADigiDoc/ViewModel/RecentDocumentsViewModel.swift index 4b3cf0b8..26587ab3 100644 --- a/RIADigiDoc/ViewModel/RecentDocumentsViewModel.swift +++ b/RIADigiDoc/ViewModel/RecentDocumentsViewModel.swift @@ -35,17 +35,20 @@ class RecentDocumentsViewModel: RecentDocumentsViewModelProtocol { private let sharedContainerViewModel: SharedContainerViewModelProtocol private let fileManager: FileManagerProtocol + private let fileInspector: FileInspectorProtocol init( sharedContainerViewModel: SharedContainerViewModelProtocol, - fileManager: FileManagerProtocol + fileManager: FileManagerProtocol, + fileInspector: FileInspectorProtocol ) { self.sharedContainerViewModel = sharedContainerViewModel self.fileManager = fileManager + self.fileInspector = fileInspector } func filteredFiles(using extensions: [String]) -> [FileItem] { - let sortedFiles = files.sorted { $0.modifiedDate > $1.modifiedDate } + let sortedFiles = files.sorted { $0.lastOpened > $1.lastOpened } return sortedFiles.filter { file in extensions.contains(file.url.pathExtension.lowercased()) && (searchText.isEmpty || file.name.localizedCaseInsensitiveContains(searchText)) @@ -63,17 +66,21 @@ class RecentDocumentsViewModel: RecentDocumentsViewModelProtocol { let fileURLs = try fileManager.contentsOfDirectory( at: folderURL, - includingPropertiesForKeys: [.contentModificationDateKey], + includingPropertiesForKeys: [.contentAccessDateKey], options: .skipsHiddenFiles ) - files = fileURLs.compactMap { url in - if let attributes = try? fileManager.attributesOfItem(atPath: url.resolvedPath), - let modifiedDate = attributes[.modificationDate] as? Date { - if extensions.contains(url.pathExtension.lowercased()) { - return FileItem(name: url.lastPathComponent, url: url, modifiedDate: modifiedDate) - } + files = try fileURLs.compactMap { url in + guard extensions.contains(url.pathExtension.lowercased()) else { + return nil } - return nil + + let lastOpened = try fileInspector.lastOpened(for: url) + + return FileItem( + name: url.lastPathComponent, + url: url, + lastOpened: lastOpened + ) } } catch { files = [] diff --git a/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift b/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift index 968d0296..7d9d532a 100644 --- a/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift @@ -50,17 +50,20 @@ class MobileIdViewModel: MobileIdViewModelProtocol { private let mobileIdSignService: MobileIdSignServiceProtocol private let certificateUtil: CertificateUtilProtocol private let dataStore: DataStoreProtocol + private let proxyUtil: ProxyUtilProtocol init( configurationRepository: ConfigurationRepositoryProtocol, mobileIdSignService: MobileIdSignServiceProtocol, certificateUtil: CertificateUtilProtocol, - dataStore: DataStoreProtocol + dataStore: DataStoreProtocol, + proxyUtil: ProxyUtilProtocol ) { self.configurationRepository = configurationRepository self.mobileIdSignService = mobileIdSignService self.certificateUtil = certificateUtil self.dataStore = dataStore + self.proxyUtil = proxyUtil } func isSigningEnabled( @@ -128,6 +131,8 @@ class MobileIdViewModel: MobileIdViewModelProtocol { let trustedCertificates = getTrustedCertificates(certificates: certBundle) + let proxyInfo = await proxyUtil.getProxyInfo() + let containerFile: URL do { containerFile = try await getContainerFile(signedContainer: signedContainer) @@ -143,7 +148,8 @@ class MobileIdViewModel: MobileIdViewModelProtocol { midUrl: midUrl, phoneNumber: phoneNumber, personalCode: personalCode, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } catch { MobileIdViewModel.logger.debug("Mobile-ID: Unable to request certificate or get cert from response") @@ -187,7 +193,8 @@ class MobileIdViewModel: MobileIdViewModelProtocol { personalCode: personalCode, hash: hash, language: language, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } catch { MobileIdViewModel.logger.debug("Mobile-ID: Unable to request signature") @@ -200,7 +207,8 @@ class MobileIdViewModel: MobileIdViewModelProtocol { signatureData = try await requestSession( midUrl: midUrl, sessionId: sessionId, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } catch { MobileIdViewModel.logger.debug( @@ -236,7 +244,8 @@ class MobileIdViewModel: MobileIdViewModelProtocol { midUrl: URL, phoneNumber: String, personalCode: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> Data { MobileIdViewModel.logger.debug("Mobile-ID: Getting certificate") let certResponse = try await mobileIdSignService @@ -246,7 +255,8 @@ class MobileIdViewModel: MobileIdViewModelProtocol { relyingPartyUUID: Constants.Signing.RelyingPartyUUID, phoneNumber: phoneNumber, nationalIdentityNumber: personalCode, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) guard let certData = Data( @@ -267,7 +277,8 @@ class MobileIdViewModel: MobileIdViewModelProtocol { personalCode: String, hash: Data, language: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> String { MobileIdViewModel.logger.debug("Mobile-ID: Getting signature") let signatureResponse = try await mobileIdSignService.getSignatureRequest( @@ -281,7 +292,8 @@ class MobileIdViewModel: MobileIdViewModelProtocol { language: getThreeLetterLanguage(from: language), displayText: NSLocalizedString("Sign document", comment: ""), displayTextFormat: Constants.MobileId.DisplayTextFormat, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) MobileIdViewModel.logger.debug("Mobile-ID: Getting sessionID") @@ -297,14 +309,16 @@ class MobileIdViewModel: MobileIdViewModelProtocol { private func requestSession( midUrl: URL, sessionId: String, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> Data { MobileIdViewModel.logger.debug("Mobile-ID: Getting session request") let sessionResponse = try await mobileIdSignService.getSessionRequest( url: "\(midUrl)\(MobileIdViewModel.signatureSessionEndpoint)", sessionId: sessionId, pollingTimeout: Constants.Signing.DefaultTimeout, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) guard let signatureData = sessionResponse.signature?.value else { diff --git a/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift b/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift index bac6b19b..14a156c0 100644 --- a/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift @@ -56,19 +56,22 @@ class SmartIdViewModel: SmartIdViewModelProtocol { private let certificateUtil: CertificateUtilProtocol private let notificationUtil: NotificationUtilProtocol private let dataStore: DataStoreProtocol + private let proxyUtil: ProxyUtilProtocol init( configurationRepository: ConfigurationRepositoryProtocol, smartIdSignService: SmartIdSignServiceProtocol, certificateUtil: CertificateUtilProtocol, notificationUtil: NotificationUtilProtocol, - dataStore: DataStoreProtocol + dataStore: DataStoreProtocol, + proxyUtil: ProxyUtilProtocol ) { self.configurationRepository = configurationRepository self.smartIdSignService = smartIdSignService self.certificateUtil = certificateUtil self.notificationUtil = notificationUtil self.dataStore = dataStore + self.proxyUtil = proxyUtil } func appDidEnterBackground() { @@ -141,6 +144,8 @@ class SmartIdViewModel: SmartIdViewModelProtocol { let trustedCertificates = getTrustedCertificates(certificates: certBundle) + let proxyInfo = await proxyUtil.getProxyInfo() + let containerFile: URL do { containerFile = try await getContainerFile(signedContainer: signedContainer) @@ -159,7 +164,8 @@ class SmartIdViewModel: SmartIdViewModelProtocol { country: country, personalCode: personalCode, pollingTimeout: Constants.Signing.DefaultTimeout, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } catch { SmartIdViewModel.logger.debug("Smart-ID: Unable to request certificate or get response") @@ -228,7 +234,8 @@ class SmartIdViewModel: SmartIdViewModelProtocol { allowedInteractionsOrderType: "confirmationMessageAndVerificationCodeChoice", displayText: NSLocalizedString("Sign document", comment: ""), pollingTimeout: Constants.Signing.DefaultTimeout, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } catch { endBackgroundTask() @@ -283,12 +290,14 @@ class SmartIdViewModel: SmartIdViewModelProtocol { } } + // swiftlint:disable:next function_parameter_count private func requestCertificate( sidUrl: URL, country: SmartIdCountry, personalCode: String, pollingTimeout: Int, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> SmartIdSessionResponse { SmartIdViewModel.logger.debug("Smart-ID: Getting certificate") let certResponse = try await smartIdSignService @@ -298,7 +307,8 @@ class SmartIdViewModel: SmartIdViewModelProtocol { relyingPartyUUID: Constants.Signing.RelyingPartyUUID, country: getCountry(smartIdCountry: country), nationalIdentityNumber: personalCode, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) guard let sessionId = certResponse.sessionID else { @@ -311,7 +321,8 @@ class SmartIdViewModel: SmartIdViewModelProtocol { sidUrl: "\(sidUrl)\(SmartIdViewModel.sessionEndpoint)", sessionId: sessionId, pollingTimout: pollingTimeout, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } @@ -324,7 +335,8 @@ class SmartIdViewModel: SmartIdViewModelProtocol { allowedInteractionsOrderType: String, displayText: String, pollingTimeout: Int, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> Data { SmartIdViewModel.logger.debug("Smart-ID: Getting signature") @@ -338,7 +350,8 @@ class SmartIdViewModel: SmartIdViewModelProtocol { hashType: hashType, allowedInteractionsOrderType: allowedInteractionsOrderType, displayText200: displayText, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) guard let sessionId = certResponse.sessionID else { @@ -351,7 +364,8 @@ class SmartIdViewModel: SmartIdViewModelProtocol { sidUrl: "\(sidUrl)\(SmartIdViewModel.sessionEndpoint)", sessionId: sessionId, pollingTimout: pollingTimeout, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) guard let signature = sessionResponse.signature?.value else { @@ -378,14 +392,16 @@ class SmartIdViewModel: SmartIdViewModelProtocol { sidUrl: String, sessionId: String, pollingTimout: Int, - trustedCertificates: [SecCertificate] + trustedCertificates: [SecCertificate], + proxyInfo: ProxyInfo ) async throws -> SmartIdSessionResponse { SmartIdViewModel.logger.debug("Smart-ID: Getting session") return try await smartIdSignService.getSessionRequest( url: sidUrl, sessionId: sessionId, pollingTimeout: pollingTimout, - trustedCertificates: trustedCertificates + trustedCertificates: trustedCertificates, + proxyInfo: proxyInfo ) } diff --git a/RIADigiDoc/ViewModel/SigningViewModel.swift b/RIADigiDoc/ViewModel/SigningViewModel.swift index 8c97a262..7b006a0c 100644 --- a/RIADigiDoc/ViewModel/SigningViewModel.swift +++ b/RIADigiDoc/ViewModel/SigningViewModel.swift @@ -201,6 +201,14 @@ class SigningViewModel: SigningViewModelProtocol { sharedContainerViewModel.getIsSignatureAdded() } + public func removeLastOpenedXattr(from url: URL) { + do { + try url.removeLastOpenedXattr() + } catch { + SigningViewModel.logger.error("Unable to remove last opened xattr: \(error)") + } + } + private func validateFiles(_ files: [URL]) throws { guard let firstFile = files.first else { throw FileOpeningError.noDataFiles @@ -254,6 +262,7 @@ class SigningViewModel: SigningViewModelProtocol { var totalFilesCount = 0 var failedFileCount = 0 + var duplicateFileCount = 0 guard let digiDocError = error as? DigiDocError else { errorMessage = ErrorMessage(key: "General error", args: []) @@ -264,8 +273,14 @@ class SigningViewModel: SigningViewModelProtocol { 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 + + if duplicateFileCount > 1 { + errorMessage = ErrorMessage(key: errorDetail.message, args: [String(duplicateFileCount)]) + } else { + errorMessage = ErrorMessage(key: errorDetail.message, args: [String(failedFileCount)]) + } - errorMessage = ErrorMessage(key: errorDetail.message, args: [String(failedFileCount)]) default: errorMessage = ErrorMessage(key: "General error", args: []) } @@ -291,7 +306,11 @@ class SigningViewModel: SigningViewModelProtocol { @discardableResult public func renameContainer(to newName: String) async -> URL? { do { - return try await signedContainer?.renameContainer(to: newName) + let renamedContainer = try await signedContainer?.renameContainer(to: newName) + sharedContainerViewModel.removeLastContainer() + sharedContainerViewModel.setSignedContainer(renamedContainer) + await loadContainerData(signedContainer: renamedContainer) + return await renamedContainer?.getRawContainerFile() } catch { SigningViewModel.logger.error("Unable to rename container: \(error)") if let digiDocError = error as? DigiDocError { diff --git a/RIADigiDocTests/ViewModel/MobileIDSmartIDSettingsViewModelTests.swift b/RIADigiDocTests/ViewModel/MobileIDSmartIDSettingsViewModelTests.swift index a3bdb2c3..4c091fe4 100644 --- a/RIADigiDocTests/ViewModel/MobileIDSmartIDSettingsViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/MobileIDSmartIDSettingsViewModelTests.swift @@ -73,18 +73,16 @@ final class MobileIDSmartIDSettingsViewModelTests { @Test func saveSettings_successWithManualSettingWithValidUUID() async throws { + let testUUID = "00000000-0000-0000-0000-000000000001" let viewModel = MobileIDSmartIDSettingsViewModel(dataStore: mockDataStore) mockDataStore.getRelyingPartyOptionHandler = { return .manualSetting } - await viewModel.loadSettings() - - viewModel.selectedOption = .manualSetting - let testUUID = "00000000-0000-0000-0000-000000000001" - viewModel.relyingPartyUUID = testUUID + mockDataStore.getRelyingPartyUUIDHandler = { testUUID } + await viewModel.loadSettings() await viewModel.saveSettings() #expect(mockDataStore.setRelyingPartyUUIDCallCount == 1) diff --git a/RIADigiDocTests/ViewModel/RecentDocumentsViewModelTests.swift b/RIADigiDocTests/ViewModel/RecentDocumentsViewModelTests.swift index d15f313d..3aa5136e 100644 --- a/RIADigiDocTests/ViewModel/RecentDocumentsViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/RecentDocumentsViewModelTests.swift @@ -25,8 +25,9 @@ import CommonsLibMocks @MainActor class RecentDocumentsViewModelTests { - private let mockSharedContainerViewModel: SharedContainerViewModelProtocolMock! - private let mockFileManager: FileManagerProtocolMock! + private let mockSharedContainerViewModel: SharedContainerViewModelProtocolMock + private let mockFileManager: FileManagerProtocolMock + private let mockFileInspector: FileInspectorProtocolMock private let viewModel: RecentDocumentsViewModel! @@ -35,17 +36,19 @@ class RecentDocumentsViewModelTests { init() async throws { mockSharedContainerViewModel = SharedContainerViewModelProtocolMock() mockFileManager = FileManagerProtocolMock() + mockFileInspector = FileInspectorProtocolMock() tempFolderURL = URL(fileURLWithPath: "/mock/path") viewModel = RecentDocumentsViewModel( sharedContainerViewModel: mockSharedContainerViewModel, - fileManager: mockFileManager + fileManager: mockFileManager, + fileInspector: mockFileInspector ) } @Test - func loadFiles_success() async { + func loadFiles_success() async throws { let file1 = tempFolderURL.appending(path: "test1.asice") let file2 = tempFolderURL.appending(path: "test2.bdoc") let invalidFile = tempFolderURL.appending(path: "invalid.txt") @@ -54,16 +57,18 @@ class RecentDocumentsViewModelTests { return [file1, file2, invalidFile] } - mockFileManager.attributesOfItemHandler = { path in - if path == file1.resolvedPath || path == file2.resolvedPath { - return [.modificationDate: Date()] - } - - throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoSuchFileError, userInfo: nil) - } +// mockFileManager.attributesOfItemHandler = { path in +// if path == file1.resolvedPath || path == file2.resolvedPath { +// return [.modificationDate: Date()] +// } +// +// throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoSuchFileError, userInfo: nil) +// } mockFileManager.fileExistsAtPathHandler = { _, _ in true } + mockFileInspector.lastOpenedHandler = { _ in Date() } + viewModel.loadFiles(from: tempFolderURL, withExtensions: Constants.Container.ContainerExtensions) let files = viewModel.files @@ -82,7 +87,8 @@ class RecentDocumentsViewModelTests { func loadFiles_emptyFilesWhenNoFolderLocation() async { let recentDocumentsViewModel = RecentDocumentsViewModel( sharedContainerViewModel: mockSharedContainerViewModel, - fileManager: mockFileManager + fileManager: mockFileManager, + fileInspector: mockFileInspector ) recentDocumentsViewModel.loadFiles(from: tempFolderURL, withExtensions: Constants.Container.ContainerExtensions) @@ -96,7 +102,8 @@ class RecentDocumentsViewModelTests { func loadFiles_emptyFilesWhenInvalidFolderLocation() async { let recentDocumentsViewModel = RecentDocumentsViewModel( sharedContainerViewModel: mockSharedContainerViewModel, - fileManager: mockFileManager + fileManager: mockFileManager, + fileInspector: mockFileInspector ) recentDocumentsViewModel.loadFiles(from: tempFolderURL, withExtensions: Constants.Container.ContainerExtensions) @@ -110,17 +117,17 @@ class RecentDocumentsViewModelTests { func filteredFiles_successWithFilteringContainerFiles() async { let date = Date() viewModel.files = [ - FileItem(name: "File1.asice", url: URL(fileURLWithPath: "file1.asice"), modifiedDate: date), + FileItem(name: "File1.asice", url: URL(fileURLWithPath: "file1.asice"), lastOpened: date), FileItem( name: "File2.bdoc", url: URL( fileURLWithPath: "file2.bdoc" ), - modifiedDate: date.addingTimeInterval( + lastOpened: date.addingTimeInterval( -1000 ) ), - FileItem(name: "test.txt", url: URL(fileURLWithPath: "test.txt"), modifiedDate: date) + FileItem(name: "test.txt", url: URL(fileURLWithPath: "test.txt"), lastOpened: date) ] viewModel.searchText = "file" @@ -155,7 +162,7 @@ class RecentDocumentsViewModelTests { func deleteFile_success() { let file = tempFolderURL.appending(path: "test1.asice") - let fileItem = FileItem(name: "test1.asice", url: file, modifiedDate: Date()) + let fileItem = FileItem(name: "test1.asice", url: file, lastOpened: Date()) viewModel.files = [fileItem] mockFileManager.removeItemHandler = { _ in } @@ -170,7 +177,7 @@ class RecentDocumentsViewModelTests { @Test func deleteFile_filesNotChangedWhenUnableToDelete() async { let file = tempFolderURL.appending(path: "test1.asice") - let fileItem = FileItem(name: "test1.asice", url: file, modifiedDate: Date()) + let fileItem = FileItem(name: "test1.asice", url: file, lastOpened: Date()) viewModel.files = [fileItem] mockFileManager.removeItemHandler = { _ in diff --git a/RIADigiDocTests/ViewModel/Signing/MobileId/MobileIdViewModelTests.swift b/RIADigiDocTests/ViewModel/Signing/MobileId/MobileIdViewModelTests.swift index 97dbb1fd..c1d9bb6d 100644 --- a/RIADigiDocTests/ViewModel/Signing/MobileId/MobileIdViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/Signing/MobileId/MobileIdViewModelTests.swift @@ -30,6 +30,7 @@ struct MobileIdViewModelTests { private let mockMobileIdSignService: MobileIdSignServiceProtocolMock private let mockCertificatUtil: CertificateUtilProtocolMock private let mockDataStore: DataStoreProtocolMock + private let mockProxyUtil: ProxyUtilProtocolMock private let viewModel: MobileIdViewModel @@ -38,12 +39,14 @@ struct MobileIdViewModelTests { self.mockMobileIdSignService = MobileIdSignServiceProtocolMock() self.mockCertificatUtil = CertificateUtilProtocolMock() self.mockDataStore = DataStoreProtocolMock() + self.mockProxyUtil = ProxyUtilProtocolMock() viewModel = MobileIdViewModel( configurationRepository: mockConfigurationRepository, mobileIdSignService: mockMobileIdSignService, certificateUtil: mockCertificatUtil, - dataStore: mockDataStore + dataStore: mockDataStore, + proxyUtil: mockProxyUtil ) } diff --git a/RIADigiDocTests/ViewModel/Signing/SmartId/SmartIdViewModelTests.swift b/RIADigiDocTests/ViewModel/Signing/SmartId/SmartIdViewModelTests.swift index d5240b83..9fb0e8b5 100644 --- a/RIADigiDocTests/ViewModel/Signing/SmartId/SmartIdViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/Signing/SmartId/SmartIdViewModelTests.swift @@ -32,6 +32,7 @@ struct SmartIdViewModelTests { private let mockCertificatUtil: CertificateUtilProtocolMock private let mockNotificationUtil: NotificationUtilProtocolMock private let mockDataStore: DataStoreProtocolMock + private let mockProxyUtil: ProxyUtilProtocolMock private let viewModel: SmartIdViewModel @@ -41,13 +42,15 @@ struct SmartIdViewModelTests { self.mockCertificatUtil = CertificateUtilProtocolMock() self.mockNotificationUtil = NotificationUtilProtocolMock() self.mockDataStore = DataStoreProtocolMock() + self.mockProxyUtil = ProxyUtilProtocolMock() viewModel = SmartIdViewModel( configurationRepository: mockConfigurationRepository, smartIdSignService: mockSmartIdSignService, certificateUtil: mockCertificatUtil, notificationUtil: mockNotificationUtil, - dataStore: mockDataStore + dataStore: mockDataStore, + proxyUtil: mockProxyUtil ) } diff --git a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift index 4b73d3c7..87bf76bf 100644 --- a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift @@ -263,11 +263,12 @@ struct SigningViewModelTests { let expectedURL = URL(fileURLWithPath: "/tmp/renamed.asice") let mockSignedContainer = SignedContainerProtocolMock() - mockSignedContainer.renameContainerHandler = { _ in expectedURL } + mockSignedContainer.getRawContainerFileHandler = { expectedURL } + mockSignedContainer.renameContainerHandler = { _ in mockSignedContainer } await viewModel.loadContainerData(signedContainer: mockSignedContainer) - let result = await viewModel.renameContainer(to: "renamed.asice") + let result = await viewModel.renameContainer(to: expectedURL.lastPathComponent) #expect(result == expectedURL) #expect(viewModel.errorMessage == nil) @@ -1332,7 +1333,7 @@ struct SigningViewModelTests { print(signedContainer) - #expect(viewModel.errorMessage == ErrorMessage(key: "Could not add files", args: ["2"])) + #expect(viewModel.errorMessage == ErrorMessage(key: "Multiple documents already exist", args: ["2"])) #expect(viewModel.dataFiles.count == 3) } @@ -1387,7 +1388,7 @@ struct SigningViewModelTests { testFile, testFile2 ], to: containerFile) - #expect(viewModel.errorMessage == ErrorMessage(key: "Could not add files", args: ["2"])) + #expect(viewModel.errorMessage == ErrorMessage(key: "Multiple documents already exist", args: ["2"])) #expect(viewModel.dataFiles.count == 2) } } diff --git a/codemagic.yaml b/codemagic.yaml index 56615a04..b3ed5198 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -267,7 +267,6 @@ workflows: echo "Moving TSLs to app's TSL files directory" mv -v scripts/TSL/* $TSL_FILES_DIRECTORY echo "Done moving TSLs" - - *get_app_version - name: Fetch default configuration script: | echo "Cleaning swift-sh cache" @@ -284,11 +283,13 @@ workflows: --project RIADigiDoc.xcodeproj \ --scheme AllTests \ --device "iPhone 17" + - *get_app_version - name: Increment build number script: | LATEST_BUILD_NUMBER=$(app-store-connect get-latest-build-number $APP_STORE_APP_ID) - echo LATEST_BUILD_NUMBER=$LATEST_BUILD_NUMBER >> $CM_ENV - agvtool new-version -all $(($LATEST_BUILD_NUMBER + 1)) + LATEST_BUILD_NUMBER=$((LATEST_BUILD_NUMBER + 1)) + echo "LATEST_BUILD_NUMBER=$LATEST_BUILD_NUMBER" >> $CM_ENV + agvtool new-version -all "$LATEST_BUILD_NUMBER" - name: Build RIA DigiDoc script: | xcode-project build-ipa \