diff --git a/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm b/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm index 4d30bd1b..abe13c7c 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm +++ b/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm @@ -111,7 +111,17 @@ + (CdocInfo*)cdocInfo:(NSString *)fullPath error:(NSError**)error { } else if(lock.isPKI()) { [addressees addObject:[[Addressee alloc] initWithLabel:lock.label pub:[NSData dataFromVector:lock.getBytes(libcdoc::Lock::RCPT_KEY)] concatKDFAlgorithmURI:@""]]; } else if(lock.isSymmetric()) { - [addressees addObject:[[Addressee alloc] initWithData:[NSData data] cnVal:[NSString stringWithStdString:lock.label]]]; + std::map info = libcdoc::Recipient::parseLabel(lock.label); + NSString *cnVal = info.contains("label") + ? [NSString stringWithStdString:info["label"]] + : @""; + [addressees addObject:[[Addressee alloc] + initWithCnVal:cnVal + serialNumber:nil + certType:CertTypePasswordType + validTo:nil + data:[NSData data] + concatKDFAlgorithmURI:@""]]; } else { [addressees addObject:[[Addressee alloc] initWithData:[NSData data] cnVal:@"Unknown capsule"]]; } diff --git a/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/Addressee.swift b/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/Addressee.swift index da278ca7..8ead66de 100644 --- a/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/Addressee.swift +++ b/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/Addressee.swift @@ -72,7 +72,7 @@ import Foundation concatKDFAlgorithmURI: String = "" ) { let split = cnVal.split(separator: ",").map { String($0) } - if split.count > 1 { + if split.count >= 3 { surname = split[0] givenName = split[1] identifier = split[2] @@ -92,7 +92,7 @@ import Foundation data = cert let cnVal = x509?.subject(oid: .commonName)?.joined(separator: ",") ?? "" let split = cnVal.split(separator: ",").map { String($0) } - if split.count > 1 { + if split.count >= 3 { surname = split[0] givenName = split[1] identifier = split[2] diff --git a/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/X509CertificateType.swift b/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/X509CertificateType.swift index 798d86c8..7b61cde3 100644 --- a/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/X509CertificateType.swift +++ b/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/X509CertificateType.swift @@ -27,6 +27,7 @@ import ASN1Decoder case mobileIDType case smartIDType case eSealType + case passwordType } extension X509Certificate { diff --git a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift index 60bed90f..79691538 100644 --- a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift +++ b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift @@ -377,6 +377,51 @@ extension CryptoContainer { ) } + @MainActor + public static func decryptWithPassword( + containerFile: URL, + recipients: [Addressee], + password: String, + fileManager: FileManagerProtocol = Container.shared.fileManager() + ) async throws -> CryptoContainerProtocol { + let path = containerFile.resolvedPath + let decryptedData: [String: Data] = try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .userInitiated).async { + do { + let result = try Decrypt.decryptFile(path, withPassword: password) + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } + + var urlDataFiles: [URL] = [] + for dataFile in decryptedData { + let sanitizedName = dataFile.key.sanitized() + let destinationPath = try Directories.getCacheDirectory( + subfolders: [Constants.Folder.ContainerFolder, Constants.Folder.Temp], + fileManager: fileManager + ) + let fileUrl = destinationPath.appending(path: sanitizedName, directoryHint: .notDirectory) + urlDataFiles.append(fileUrl) + let isCreated = fileManager.createFile( + atPath: fileUrl.resolvedPath, contents: dataFile.value, attributes: nil + ) + if !isCreated { + CryptoContainer.logger().error("Unable to create file at path: \(destinationPath.resolvedPath)") + } + } + + return try await create( + containerFile: containerFile, + dataFiles: urlDataFiles, + recipients: recipients, + isDecrypted: true, + isEncrypted: false + ) + } + @MainActor public static func encrypt( containerFile: URL, diff --git a/RIADigiDoc/Domain/Model/Crypto/EncryptRecipientViewTab.swift b/RIADigiDoc/Domain/Model/Crypto/EncryptRecipientViewTab.swift new file mode 100644 index 00000000..973d401b --- /dev/null +++ b/RIADigiDoc/Domain/Model/Crypto/EncryptRecipientViewTab.swift @@ -0,0 +1,23 @@ +/* + * Copyright 2017 - 2026 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 EncryptRecipientViewTab: Int, Sendable { + case recipient = 0 + case password = 1 +} diff --git a/RIADigiDoc/Domain/Model/Crypto/EncryptViewTab.swift b/RIADigiDoc/Domain/Model/Crypto/EncryptViewTab.swift index 950c872c..5add02e2 100644 --- a/RIADigiDoc/Domain/Model/Crypto/EncryptViewTab.swift +++ b/RIADigiDoc/Domain/Model/Crypto/EncryptViewTab.swift @@ -17,7 +17,7 @@ * */ -enum EncryptViewTab: Int, Sendable { +public enum EncryptViewTab: Int, Sendable { case files = 0 case recipients = 1 } diff --git a/RIADigiDoc/Domain/Model/EncryptionCdocOption.swift b/RIADigiDoc/Domain/Model/EncryptionCdocOption.swift index bdced805..a4e37ba8 100644 --- a/RIADigiDoc/Domain/Model/EncryptionCdocOption.swift +++ b/RIADigiDoc/Domain/Model/EncryptionCdocOption.swift @@ -17,7 +17,7 @@ * */ -public enum EncryptionCdocOption: Int, Sendable { +public enum EncryptionCdocOption: Int, Sendable, Hashable { case cdoc1 = 0 case cdoc2 = 1 } diff --git a/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift b/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift index 0b2a7a24..1465fce3 100644 --- a/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift +++ b/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift @@ -30,7 +30,7 @@ public enum NavigationDestination: Hashable { extensions: [String] ) - case encryptRecipientView + case encryptRecipientView(cdocOption: EncryptionCdocOption) case signingView case signatureDetailView( @@ -51,8 +51,11 @@ public enum NavigationDestination: Hashable { ) case encryptView( - isWithEncryption: Bool + isWithEncryption: Bool, + cdocOption: EncryptionCdocOption, + selectedTab: EncryptViewTab ) + case recipientDetailView( recipient: Addressee, ) diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index 03b639f8..4ee02038 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -1259,6 +1259,186 @@ } } }, + "Crypto password encryption description" : { + "comment" : "Encrypt with password tab description", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password encryption is intended for long-term storage. The password cannot be changed or recovered." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parooliga krüpteerimine on mõeldud pikaajalise säilitamise jaoks. Parooli ei ole võimalik muuta ega taastada." + } + } + } + }, + "Crypto password field label" : { + "comment" : "Password dialog — password field label", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Document password" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dokumendi parool" + } + } + } + }, + "Crypto password key label" : { + "comment" : "Password dialog — key label field placeholder", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Key label" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Võtme silt" + } + } + } + }, + "Crypto password key label description" : { + "comment" : "Password dialog — key label helper text shown below the field", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recipient name or ID" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adressaadi nimi või ID" + } + } + } + }, + "Crypto password length requirement" : { + "comment" : "Password dialog — length requirement", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Length: 20 – 64 characters" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pikkus: 20 – 64 tähemärki" + } + } + } + }, + "Crypto password lowercase requirement" : { + "comment" : "Password dialog — lowercase letter requirement", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contains at least one lowercase letter" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sisaldab vähemalt ühte väiketähte" + } + } + } + }, + "Crypto password number requirement" : { + "comment" : "Password dialog — number requirement", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contains at least one number (0 – 9)" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sisaldab vähemalt ühte numbrit (0 – 9)" + } + } + } + }, + "Crypto password repeat label" : { + "comment" : "Password dialog — repeat password field label", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repeat password" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Korda parooli" + } + } + } + }, + "Crypto password save warning" : { + "comment" : "Password dialog — info box warning text", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Be sure to save the password in a secure place - without the password, you won't be able to open the file again." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salvesta parool kindlasse kohta – ilma paroolita ei saa te faili enam avada." + } + } + } + }, + "Crypto password uppercase requirement" : { + "comment" : "Password dialog — uppercase letter requirement", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contains at least one uppercase letter" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sisaldab vähemalt ühte suurtähte" + } + } + } + }, "Crypto recipients description" : { "comment" : "Crypto recipients description", "extractionState" : "manual", @@ -1547,6 +1727,24 @@ } } }, + "Encrypt based on recipient" : { + "comment" : "Encrypt recipient tab title", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Encrypt based on recipient" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Krüpteeri adressaadi põhjal" + } + } + } + }, "Encrypt container" : { "comment" : "Accessibility variant of Encrypt", "extractionState" : "manual", @@ -1583,6 +1781,24 @@ } } }, + "Encrypt with password" : { + "comment" : "Encrypt with password tab title", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Encrypt with password" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Krüpteeri parooliga" + } + } + } + }, "Enter current PIN code" : { "comment" : "My eID current PIN or PUK code step title", "extractionState" : "manual", @@ -5386,6 +5602,60 @@ } } }, + "Password" : { + "comment" : "Crypto recipient password type label", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parool" + } + } + } + }, + "Password range to accessibility" : { + "comment" : "Password dialog — VoiceOver word spoken in place of the en dash in ranges like 20 – 64", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "to" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "kuni" + } + } + } + }, + "Password requirements" : { + "comment" : "Password dialog — VoiceOver heading announced before the requirement list", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password requirements" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parooli nõuded" + } + } + } + }, "Person or company does not own a valid certificate" : { "comment" : "LDAP search message when no results found", "extractionState" : "manual", diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index cafd2b14..ba757be3 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -19,6 +19,7 @@ import SwiftUI import FactoryKit +import CryptoSwift import CryptoObjCWrapper import CommonsLib import UtilsLib @@ -38,6 +39,9 @@ struct EncryptView: View { @Environment(\.dismiss) private var dismiss @State private var selectedTab: EncryptViewTab = .files @State private var selectedRecipient: Addressee? + @State private var cdocOption: EncryptionCdocOption + @State private var showDecryptPasswordModal = false + @State private var passwordDecryptKeyLabel = "" @State private var viewModel: EncryptViewModel @@ -173,10 +177,14 @@ struct EncryptView: View { init( isWithEncryption: Bool = false, isWithDecryption: Bool = false, + cdocOption: EncryptionCdocOption = .cdoc1, + selectedTab: EncryptViewTab = .files, nameUtil: NameUtilProtocol = Container.shared.nameUtil(), recipientUtil: RecipientUtilProtocol = Container.shared.recipientUtil(), fileUtil: FileUtilProtocol = Container.shared.fileUtil() ) { + _cdocOption = State(wrappedValue: cdocOption) + _selectedTab = State(wrappedValue: selectedTab) _viewModel = State(wrappedValue: Container.shared.encryptViewModel()) self.isWithEncryption = isWithEncryption self.isWithDecryption = isWithDecryption @@ -238,9 +246,14 @@ struct EncryptView: View { } } } else if viewModel.isDecryptButtonShown { - isWithEncryption = false - isWithDecryption = false - pathManager.navigate(to: .decryptRootView) + if let passwordRecipient = viewModel.recipients.first(where: { $0.certType == .passwordType }) { + passwordDecryptKeyLabel = passwordRecipient.identifier + showDecryptPasswordModal = true + } else { + isWithEncryption = false + isWithDecryption = false + pathManager.navigate(to: .decryptRootView) + } } }, onSaveContainerButtonClick: { @@ -358,6 +371,7 @@ struct EncryptView: View { .environment(languageSettings) } } + .padding(.top, Dimensions.Padding.LPadding) } } } @@ -391,7 +405,7 @@ struct EncryptView: View { rightButtonAccessibilityLabel: rightButtonLabel.lowercased(), rightButtonAction: { if viewModel.isContainerUnencrypted { - pathManager.replaceLast(to: .encryptRecipientView) + pathManager.replaceLast(to: .encryptRecipientView(cdocOption: cdocOption)) } else { if encryptionButtonEnabled { encryptionButtonEnabled = false @@ -466,6 +480,18 @@ struct EncryptView: View { } ) + if showDecryptPasswordModal { + DecryptPasswordModalView( + keyLabel: passwordDecryptKeyLabel, + onDecrypt: { password in + // Add password decryption functionality + }, + onCancel: { + showDecryptPasswordModal = false + } + ) + } + if showRenameModal { RenameModalView( containerName: containerName, diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Modal/DecryptPasswordModalView.swift b/RIADigiDoc/UI/Component/Container/Crypto/Modal/DecryptPasswordModalView.swift new file mode 100644 index 00000000..8fc3da9b --- /dev/null +++ b/RIADigiDoc/UI/Component/Container/Crypto/Modal/DecryptPasswordModalView.swift @@ -0,0 +1,104 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import SwiftUI +import FactoryKit + +struct DecryptPasswordModalView: View { + @Environment(LanguageSettings.self) private var languageSettings + @AppTheme private var theme + @AppTypography private var typography + + @State private var password: String = "" + + let keyLabel: String + let onDecrypt: (String) -> Void + let onCancel: () -> Void + + private var passwordFieldTitle: String { + languageSettings.localized("Crypto password field label") + } + + var body: some View { + PasswordModalCard { + VStack(alignment: .leading, spacing: Dimensions.Padding.MPadding) { + ViewThatFits(in: .vertical) { + dialogContent + + ScrollView { + dialogContent + } + } + PasswordModalButtonRow( + cancelLabel: languageSettings.localized("Cancel"), + confirmLabel: languageSettings.localized("Decrypt"), + onCancel: onCancel, + onConfirm: { onDecrypt(password) } + ) + } + } + } + + private var dialogContent: some View { + VStack(alignment: .leading, spacing: Dimensions.Padding.MPadding) { + PasswordModalTitleView(text: languageSettings.localized("Decrypt")) + keyLabelSection + passwordSection + } + } + + private var keyLabelTitle: String { + languageSettings.localized("Crypto password key label") + } + + private var keyLabelSection: some View { + VStack(alignment: .leading, spacing: Dimensions.Padding.XXSPadding) { + Text(verbatim: keyLabelTitle) + .font(typography.bodyLarge) + .foregroundStyle(theme.onSurfaceVariant) + .accessibilityHidden(true) + Text(verbatim: keyLabel) + .font(typography.bodyLarge) + .foregroundStyle(theme.onSurface) + .fontWeight(.bold) + .accessibilityLabel(Text(verbatim: "\(keyLabelTitle) \(keyLabel)")) + } + } + + private var passwordSection: some View { + FloatingLabelTextField( + title: passwordFieldTitle, + placeholder: passwordFieldTitle, + text: $password, + isSecure: true, + submitLabel: .done, + identifier: "decryptPasswordInput" + ) + } +} + +#Preview { + DecryptPasswordModalView( + keyLabel: "Allkirjastamata lepingud 2026", + onDecrypt: { _ in }, + onCancel: {} + ) + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) +} diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Modal/EncryptPasswordModalView.swift b/RIADigiDoc/UI/Component/Container/Crypto/Modal/EncryptPasswordModalView.swift new file mode 100644 index 00000000..6aee5128 --- /dev/null +++ b/RIADigiDoc/UI/Component/Container/Crypto/Modal/EncryptPasswordModalView.swift @@ -0,0 +1,172 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import SwiftUI +import FactoryKit + +struct EncryptPasswordModalView: View { + @Environment(LanguageSettings.self) private var languageSettings + @AppTheme private var theme + @AppTypography private var typography + + @State private var keyLabel: String = "" + @State private var password: String = "" + @State private var repeatPassword: String = "" + + let onEncrypt: (String, String, String) -> Void + let onCancel: () -> Void + + private var keyLabelTitle: String { languageSettings.localized("Crypto password key label") } + private var passwordTitle: String { languageSettings.localized("Crypto password field label") } + private var repeatTitle: String { languageSettings.localized("Crypto password repeat label") } + + var body: some View { + PasswordModalCard { + VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { + ScrollView { + VStack(alignment: .leading, spacing: Dimensions.Padding.MPadding) { + PasswordModalTitleView(text: languageSettings.localized("Encrypt with password")) + keyLabelSection + infoBox + passwordSection + repeatPasswordSection + } + .padding(.vertical, Dimensions.Padding.MSPadding) + } + PasswordModalButtonRow( + cancelLabel: languageSettings.localized("Cancel"), + confirmLabel: languageSettings.localized("Encrypt"), + onCancel: onCancel, + onConfirm: { onEncrypt(keyLabel, password, repeatPassword) } + ) + } + .frame(maxHeight: .infinity) + } + } + + private var keyLabelSection: some View { + VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { + FloatingLabelTextField( + title: keyLabelTitle, + placeholder: keyLabelTitle, + text: $keyLabel, + submitLabel: .next, + identifier: "passwordKeyLabel", + accessibilityHint: languageSettings.localized("Crypto password key label description") + ) + Text(verbatim: languageSettings.localized("Crypto password key label description")) + .font(typography.labelMedium) + .foregroundStyle(theme.onSecondaryContainer) + .padding(.top, Dimensions.Padding.XXSPadding) + .accessibilityHidden(true) + } + } + + private var infoBox: some View { + HStack(alignment: .center, spacing: Dimensions.Padding.SPadding) { + Image("ic_m3_info_48pt_wght400") + .resizable() + .scaledToFit() + .frame( + width: Dimensions.Icon.IconSizeXXS, + height: Dimensions.Icon.IconSizeXXS + ) + .foregroundStyle(theme.primary) + .accessibilityHidden(true) + + Text(verbatim: languageSettings.localized("Crypto password save warning")) + .font(typography.bodyLarge) + .foregroundStyle(theme.onSurface) + .fixedSize(horizontal: false, vertical: true) + } + .padding(Dimensions.Padding.SPadding) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: Dimensions.Corner.XSCornerRadius) + .fill(theme.surfaceVariant) + ) + } + + private let requirementKeys = [ + "Crypto password length requirement", + "Crypto password number requirement", + "Crypto password uppercase requirement", + "Crypto password lowercase requirement" + ] + + private var requirementsAccessibilityLabel: String { + ([languageSettings.localized("Password requirements")] + + requirementKeys.map { languageSettings.localized($0) }) + .joined(separator: ". ") + .replacingOccurrences( + of: "–", + with: " \(languageSettings.localized("Password range to accessibility")) " + ) + } + + private var passwordSection: some View { + VStack(alignment: .leading, spacing: Dimensions.Padding.MSPadding) { + FloatingLabelTextField( + title: passwordTitle, + placeholder: passwordTitle, + text: $password, + isSecure: true, + submitLabel: .next, + identifier: "passwordInput", + sortPriority: 0 + ) + VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { + ForEach(requirementKeys, id: \.self) { key in + requirementRow(key) + } + } + .accessibilityElement(children: .combine) + .accessibilityLabel(Text(verbatim: requirementsAccessibilityLabel)) + .accessibilitySortPriority(1) + } + .accessibilityElement(children: .contain) + } + + private var repeatPasswordSection: some View { + FloatingLabelTextField( + title: repeatTitle, + placeholder: repeatTitle, + text: $repeatPassword, + isSecure: true, + submitLabel: .done, + identifier: "repeatPasswordInput" + ) + } + + private func requirementRow(_ key: String) -> some View { + Text(verbatim: "• \(languageSettings.localized(key))") + .font(typography.labelMedium) + .foregroundStyle(theme.onSecondaryContainer) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +#Preview { + EncryptPasswordModalView( + onEncrypt: { _, _, _ in }, + onCancel: {} + ) + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) +} diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Modal/PasswordModalCard.swift b/RIADigiDoc/UI/Component/Container/Crypto/Modal/PasswordModalCard.swift new file mode 100644 index 00000000..e1c238c6 --- /dev/null +++ b/RIADigiDoc/UI/Component/Container/Crypto/Modal/PasswordModalCard.swift @@ -0,0 +1,120 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import SwiftUI +import FactoryKit + +struct PasswordModalCard: View { + @AppTheme private var theme + + @State private var keyboardHeight: CGFloat = 0 + + @ViewBuilder let content: () -> Content + + var body: some View { + ZStack { + Color.black + .opacity(Dimensions.Shadow.LOpacity) + .ignoresSafeArea() + .accessibilityHidden(true) + .allowsHitTesting(true) + + content() + .padding(Dimensions.Padding.MPadding) + .background( + RoundedRectangle(cornerRadius: Dimensions.Corner.MCornerRadius) + .fill(theme.surface) + ) + .padding(.horizontal, Dimensions.Padding.MPadding) + .padding(.vertical, Dimensions.Padding.XLPadding) + .offset(y: -keyboardHeight / 2) + .animation( + .easeOut(duration: Dimensions.Duration.focusAnimation), + value: keyboardHeight + ) + .accessibilityAddTraits([.isModal]) + } + .ignoresSafeArea(.keyboard) + .onReceive( + NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification) + ) { notification in + guard let frame = notification.userInfo?[ + UIResponder.keyboardFrameEndUserInfoKey + ] as? CGRect else { return } + keyboardHeight = frame.height + } + .onReceive( + NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification) + ) { _ in + keyboardHeight = 0 + } + } +} + +struct PasswordModalTitleView: View { + @AppTheme private var theme + @AppTypography private var typography + + @AccessibilityFocusState private var isFocused: Bool + + let text: String + + var body: some View { + Text(verbatim: text) + .foregroundStyle(theme.onSurface) + .font(typography.headlineSmall) + .fixedSize(horizontal: false, vertical: true) + .minimumScaleFactor(0.5) + .accessibilityHeading(.h1) + .accessibilityAddTraits([.isHeader]) + .accessibilityFocused($isFocused) + .onAppear { + Task { + try? await Task.sleep(for: .seconds(0.3)) + isFocused = true + } + } + } +} + +struct PasswordModalButtonRow: View { + @Environment(LanguageSettings.self) private var languageSettings + @AppTheme private var theme + @AppTypography private var typography + + let cancelLabel: String + let confirmLabel: String + let onCancel: () -> Void + let onConfirm: () -> Void + + var body: some View { + HStack(spacing: Dimensions.Padding.MPadding) { + Button(cancelLabel) { onCancel() } + .font(typography.labelLarge) + .foregroundStyle(theme.primary) + .minimumScaleFactor(0.5) + Button(confirmLabel) { onConfirm() } + .font(typography.labelLarge) + .foregroundStyle(theme.primary) + .minimumScaleFactor(0.5) + } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.vertical, Dimensions.Padding.MSPadding) + } +} diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift index 564e9df9..3ff9d88b 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift @@ -45,13 +45,17 @@ struct EncryptRecipientView: View { @State private var selectedRecipient: Addressee? @State private var showRemoveRecipientModal = false + @State private var showPasswordEncryptModal = false @State private var addedRecipients: [Addressee] = [] + @State private var selectedTab: EncryptRecipientViewTab = .recipient + @State private var cdocOption: EncryptionCdocOption @State private var viewModel: EncryptRecipientViewModel @State private var encryptViewModel: EncryptViewModel - init() { + init(cdocOption: EncryptionCdocOption) { + _cdocOption = State(wrappedValue: cdocOption) _viewModel = State(wrappedValue: Container.shared.encryptRecipientViewModel()) _encryptViewModel = State(wrappedValue: Container.shared.encryptViewModel()) } @@ -72,10 +76,22 @@ struct EncryptRecipientView: View { languageSettings.localized("Encrypt") } + var nextLabel: String { + languageSettings.localized("Next") + } + var noSearchResultsMessage: String { languageSettings.localized("Person or company does not own a valid certificate") } + private var recipientTabTitle: String { + languageSettings.localized("Encrypt based on recipient") + } + + private var passwordTabTitle: String { + languageSettings.localized("Encrypt with password") + } + private var addedRecipientsSection: some View { VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { if noSearchResults { @@ -127,167 +143,248 @@ struct EncryptRecipientView: View { .listSectionSpacing(.compact) } + @ViewBuilder + private var bottomButtonBar: some View { + if cdocOption == .cdoc2 && selectedTab == .password { + UnsignedBottomBarView( + showLeftButton: false, + leftButtonIconName: "", + leftButtonLabel: "", + leftButtonAccessibilityLabel: "", + leftButtonAction: {}, + rightButtonEnabled: true, + rightButtonIconName: "ic_m3_arrow_forward_48pt_wght400", + rightButtonLabel: nextLabel, + rightButtonAccessibilityLabel: nextLabel.lowercased(), + rightButtonAction: { + showPasswordEncryptModal = true + }, + showBackground: false + ) + .accessibilityIdentifier("bottomNextButton") + } else { + HStack { + Spacer() + Button(action: { + encryptionButtonEnabled = false + pathManager.replaceLast(to: .encryptView(isWithEncryption: true, cdocOption: cdocOption, selectedTab: .files)) + }, label: { + HStack(spacing: Dimensions.Padding.XSPadding) { + Image("ic_m3_encrypted_48pt_wght400") + .resizable() + .scaledToFit() + .frame( + width: Dimensions.Icon.IconSizeXXS, + height: Dimensions.Icon.IconSizeXXS + ) + .foregroundStyle(theme.onPrimaryContainer) + Text(verbatim: encryptLabel) + .foregroundStyle(theme.onPrimaryContainer) + .font(typography.bodyLarge) + } + .accessibilityHidden(true) + }) + .contentShape(Rectangle()) + .disabled(!encryptionButtonEnabled) + .padding(Dimensions.Padding.MSPadding) + .background( + RoundedRectangle(cornerRadius: Dimensions.Corner.MSCornerRadius) + .fill(theme.primaryContainer) + .shadow( + color: theme.onSurfaceVariant.opacity(Dimensions.Shadow.SOpacity), + radius: Dimensions.Shadow.radius, + x: Dimensions.Shadow.xOffset, + y: Dimensions.Shadow.yOffset + ) + ) + .padding(Dimensions.Padding.MPadding) + .accessibilityElement(children: .ignore) + .accessibilityLabel(encryptLabel.lowercased()) + .accessibilityAddTraits(.isButton) + .accessibilityIdentifier("bottomEncryptButton") + } + } + } + + @ViewBuilder + private var searchField: some View { + HStack { + Image(systemName: "magnifyingglass") + .foregroundStyle(theme.onSurfaceVariant) + .accessibilityHidden(true) + + FloatingLabelTextField( + title: "", + placeholder: languageSettings.localized("Search recipients"), + text: $viewModel.searchText, + submitLabel: .done, + identifier: "searchRecipients", + sortPriority: 1, + showBorder: false, + onDone: { + if viewModel.searchText.allSatisfy(\.isNumber) && + viewModel.searchText.count == 11 && + !PersonalCodeValidator.isPersonalCodeValid(viewModel.searchText) { + let personalCodeNotValidMessage = languageSettings.localized( + "Personal code is not valid" + ) + + Toast.show(personalCodeNotValidMessage) + + if voiceOverEnabled { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + AccessibilityUtil.announceMessage( + personalCodeNotValidMessage + ) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + isTitleFocused = true + } + } + } + return + } + + Task { + await viewModel.loadRecipients() + } + } + ) + .accessibilityFocused($isSearchFieldFocused) + .focused($isSearchFocused) + .onChange(of: isSearchFocused) { _, newValue in + isSearchExpanded = newValue + } + .onChange(of: viewModel.searchText) { + viewModel.handleSearchTextChange() + } + } + .padding(.horizontal, Dimensions.Padding.SPadding) + .background( + RoundedRectangle(cornerRadius: Dimensions.Padding.MPadding, style: .continuous) + .fill(Color(.systemGray6)) + ) + } + + @ViewBuilder + private var recipientsScrollView: some View { + ScrollView { + if noSearchResults && !isSearchExpanded { + VStack { + Text(languageSettings.localized("Crypto recipients description")) + .font(typography.bodyLarge) + .foregroundStyle(theme.onSurfaceVariant) + .multilineTextAlignment(.leading) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + } + .listStyle(.plain) + .scrollDisabled(true) + .scrollContentBackground(.hidden) + } else if showNoRecipientsFoundMessage { + emptyStateView( + languageSettings.localized( + "Person or company does not own a valid certificate" + ) + ) + } else { + filteredRecipientsSection + } + + Spacer().frame(height: Dimensions.Padding.MSPadding) + + if addedRecipients.count > 0 { + addedRecipientsSection + } + } + .accessibilitySortPriority(filteredRecipients.isEmpty ? 2 : 0) + } + + @ViewBuilder + private func containerRecipientsTitle(topPadding: CGFloat) -> some View { + if !isSearchExpanded { + Text(verbatim: languageSettings.localized("Container recipients")) + .foregroundStyle(theme.onSurface) + .font(typography.headlineSmall) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, topPadding) + .minimumScaleFactor(0.5) + .accessibilityHeading(.h1) + .accessibilityAddTraits([.isHeader]) + .accessibilityFocused($isTitleFocused) + .accessibilitySortPriority(3) + .onAppear { + if noSearchResults { isTitleFocused = true } + } + } + } + var body: some View { TopBarContainer( title: nil, onLeftClick: { - pathManager.replaceLast(to: .encryptView(isWithEncryption: false)) + pathManager.replaceLast(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .files)) }, showRightIcons: !isSearchExpanded, content: { ZStack { VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { - if !isSearchExpanded { - Text(verbatim: languageSettings.localized("Container recipients")) - .foregroundStyle(theme.onSurface) - .font(typography.headlineSmall) - .padding(.top, Dimensions.Padding.SPadding) - .minimumScaleFactor(0.5) - .accessibilityHeading(.h1) - .accessibilityAddTraits([.isHeader]) - .accessibilityFocused($isTitleFocused) - .accessibilitySortPriority(3) - .onAppear { - if noSearchResults { - isTitleFocused = true + if cdocOption == .cdoc2 { + TabView(selectedTab: $selectedTab, titles: [ + recipientTabTitle, + passwordTabTitle + ]) { + if selectedTab == .recipient { + VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { + containerRecipientsTitle(topPadding: Dimensions.Padding.MPadding) + searchField + .padding(.top, Dimensions.Padding.SPadding) + .padding(.bottom, Dimensions.Padding.SPadding) + recipientsScrollView } - } - } - HStack { - Image(systemName: "magnifyingglass") - .foregroundStyle(theme.onSurfaceVariant) - .accessibilityHidden(true) - - FloatingLabelTextField( - title: "", - placeholder: languageSettings.localized("Search recipients"), - text: $viewModel.searchText, - submitLabel: .done, - identifier: "searchRecipients", - sortPriority: 1, - showBorder: false, - onDone: { - if viewModel.searchText.allSatisfy(\.isNumber) && - viewModel.searchText.count == 11 && - !PersonalCodeValidator.isPersonalCodeValid(viewModel.searchText) { - let personalCodeNotValidMessage = languageSettings.localized( - "Personal code is not valid" - ) - - Toast.show(personalCodeNotValidMessage) - - if voiceOverEnabled { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - AccessibilityUtil.announceMessage( - personalCodeNotValidMessage - ) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - isTitleFocused = true - } - } + } else { + VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { + ScrollView { + Text(languageSettings.localized("Crypto password encryption description")) + .font(typography.bodyLarge) + .foregroundStyle(theme.onSurfaceVariant) + .multilineTextAlignment(.leading) + .padding(.top, Dimensions.Padding.MPadding) + .frame(maxWidth: .infinity, alignment: .leading) } - return - } - - Task { - await viewModel.loadRecipients() } } - ) - .accessibilityFocused($isSearchFieldFocused) - .focused($isSearchFocused) - .onChange(of: isSearchFocused) { _, newValue in - isSearchExpanded = newValue - } - .onChange(of: viewModel.searchText) { - viewModel.handleSearchTextChange() } + } else { + containerRecipientsTitle(topPadding: Dimensions.Padding.SPadding) + searchField + .padding(.top, Dimensions.Padding.LPadding) + .padding(.bottom, Dimensions.Padding.SPadding) + recipientsScrollView } - .padding(.horizontal, Dimensions.Padding.SPadding) - .background( - RoundedRectangle(cornerRadius: Dimensions.Padding.MPadding, style: .continuous) - .fill(Color(.systemGray6)) - ) - .padding(.top, Dimensions.Padding.LPadding) - .padding(.bottom, Dimensions.Padding.SPadding) - - ScrollView { - if noSearchResults && !isSearchExpanded { - VStack { - Text(languageSettings.localized("Crypto recipients description")) - .font(typography.bodyLarge) - .foregroundStyle(theme.onSurfaceVariant) - .multilineTextAlignment(.leading) - .listRowBackground(Color.clear) - .listRowSeparator(.hidden) - } - .listStyle(.plain) - .scrollDisabled(true) - .scrollContentBackground(.hidden) - } else if showNoRecipientsFoundMessage { - emptyStateView( - languageSettings.localized( - "Person or company does not own a valid certificate" - ) - ) - } else { - filteredRecipientsSection - } - Spacer().frame(height: Dimensions.Padding.MSPadding) - - if addedRecipients.count > 0 { - addedRecipientsSection - } - } - .accessibilitySortPriority(filteredRecipients.isEmpty ? 2 : 0) - - HStack { - Spacer() - - Button(action: { - encryptionButtonEnabled = false - pathManager.replaceLast(to: .encryptView(isWithEncryption: true)) - }, label: { - HStack(spacing: Dimensions.Padding.XSPadding) { - Image("ic_m3_encrypted_48pt_wght400") - .resizable() - .scaledToFit() - .frame( - width: Dimensions.Icon.IconSizeXXS, - height: Dimensions.Icon.IconSizeXXS - ) - .foregroundStyle(theme.onPrimaryContainer) - - Text(verbatim: encryptLabel) - .foregroundStyle(theme.onPrimaryContainer) - .font(typography.bodyLarge) - } - .accessibilityHidden(true) - }) - .contentShape(Rectangle()) - .disabled(!encryptionButtonEnabled) - .padding(Dimensions.Padding.MSPadding) - .background( - RoundedRectangle(cornerRadius: Dimensions.Corner.MSCornerRadius) - .fill(theme.primaryContainer) - .shadow( - color: theme.onSurfaceVariant.opacity(Dimensions.Shadow.SOpacity), - radius: Dimensions.Shadow.radius, - x: Dimensions.Shadow.xOffset, - y: Dimensions.Shadow.yOffset - ) - ) - .padding(Dimensions.Padding.MPadding) - .accessibilityElement(children: .ignore) - .accessibilityLabel(encryptLabel.lowercased()) - .accessibilityAddTraits(.isButton) - .accessibilityIdentifier("bottomEncryptButton") - } + bottomButtonBar } .padding(.horizontal, Dimensions.Padding.SPadding) .accessibilityElement(children: .contain) + if showPasswordEncryptModal { + EncryptPasswordModalView( + onEncrypt: { _, _, _ in + showPasswordEncryptModal = false + pathManager.replaceLast( + to: .encryptView( + isWithEncryption: false, + cdocOption: cdocOption, + selectedTab: .recipients + ) + ) + }, + onCancel: { + showPasswordEncryptModal = false + } + ) + } + if showRemoveRecipientModal { ConfirmModalView( title: "\(languageSettings.localized("Remove recipient"))?", @@ -417,7 +514,7 @@ struct EncryptRecipientView: View { } #Preview { - EncryptRecipientView() + EncryptRecipientView(cdocOption: .cdoc1) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) .environment(NavigationPathManager()) diff --git a/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift b/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift index ef0748ae..8b5c9a8e 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift @@ -154,6 +154,7 @@ struct RecipientDetailView: View { } } }) + .padding(.top, Dimensions.Padding.LPadding) } } .padding(Dimensions.Padding.SPadding) diff --git a/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift b/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift index 24826a96..0cb05cc8 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift @@ -75,21 +75,23 @@ struct RecipientView: View { } } + private var isPasswordRecipient: Bool { + recipient.certType == .passwordType + } + var nameText: String { - return { - if PersonalCodeValidator.isPersonalCodeValid(recipient.identifier) { - return nameUtil.formatName( - surname: recipient.surname, - givenName: recipient.givenName, - identifier: recipient.identifier - ) - } else { - return nameUtil.formatCompanyName( - identifier: recipient.identifier, - serialNumber: recipient.serialNumber - ) - } - }() + if isPasswordRecipient { return recipient.identifier } + if PersonalCodeValidator.isPersonalCodeValid(recipient.identifier) { + return nameUtil.formatName( + surname: recipient.surname, + givenName: recipient.givenName, + identifier: recipient.identifier + ) + } + return nameUtil.formatCompanyName( + identifier: recipient.identifier, + serialNumber: recipient.serialNumber + ) } var validToDate: String { @@ -147,7 +149,7 @@ struct RecipientView: View { ) let certType = recipientUtil.getRecipientCertTypeText(certType: recipient.certType) - let validPart = validToDate.isEmpty + let validPart = (validToDate.isEmpty || isPasswordRecipient) ? "" : " " + languageSettings.localized("Valid to", [validToDate]) diff --git a/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift b/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift index 5ec90543..515d224e 100644 --- a/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift +++ b/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift @@ -325,7 +325,9 @@ struct SignatureDetailView: View { ) } } + .padding(.top, Dimensions.Padding.SPadding) }) + .padding(.top, Dimensions.Padding.SPadding) } } .padding(Dimensions.Padding.SPadding) diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index 806f8a53..9c84800e 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -293,6 +293,7 @@ struct SigningView: View { .environment(languageSettings) } } + .padding(.top, Dimensions.Padding.LPadding) } else { VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { Text(verbatim: languageSettings.localized("Container files")) @@ -486,7 +487,10 @@ struct SigningView: View { .onChange(of: viewModel.navigateToNestedCryptoContainerView) { _, isNavigating in if isNavigating { viewModel.navigateToNestedCryptoContainerView.toggle() - pathManager.navigate(to: .encryptView(isWithEncryption: false)) + Task { @MainActor in + let cdocOption = await Container.shared.dataStore().getEncryptionCdocOption(false) + pathManager.navigate(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .files)) + } } } } @@ -569,8 +573,9 @@ struct SigningView: View { languageSettings.localized("Converted to crypto container"), type: .success ) + let cdocOption = await Container.shared.dataStore().getEncryptionCdocOption(false) await MainActor.run { - pathManager.replaceLast(to: .encryptView(isWithEncryption: false)) + pathManager.replaceLast(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .files)) } } } diff --git a/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift b/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift index 25fa59f2..d6b4b1a8 100644 --- a/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift +++ b/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift @@ -36,6 +36,7 @@ struct UnsignedBottomBarView: View { let rightButtonLabel: String let rightButtonAccessibilityLabel: String let rightButtonAction: () -> Void + var showBackground: Bool = true var body: some View { HStack { @@ -88,7 +89,7 @@ struct UnsignedBottomBarView: View { } .padding(.vertical, Dimensions.Padding.SPadding) .padding(.horizontal, Dimensions.Padding.MPadding) - .background(theme.surfaceContainer) + .background(showBackground ? theme.surfaceContainer : Color.clear) } } diff --git a/RIADigiDoc/UI/Component/HomeView.swift b/RIADigiDoc/UI/Component/HomeView.swift index 8c28a925..8a36ff6a 100644 --- a/RIADigiDoc/UI/Component/HomeView.swift +++ b/RIADigiDoc/UI/Component/HomeView.swift @@ -248,8 +248,11 @@ struct HomeView: View { }) .onChange(of: isNavigatingToEncryptView, { _, newValue in if newValue { - navigateWithVoiceOverFocusGuard(to: .encryptView(isWithEncryption: false)) - isNavigatingToEncryptView = false + Task { @MainActor in + let cdocOption = await Container.shared.dataStore().getEncryptionCdocOption(false) + navigateWithVoiceOverFocusGuard(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .files)) + isNavigatingToEncryptView = false + } } }) .onChange(of: isBottomSheetPresented) { oldValue, newValue in diff --git a/RIADigiDoc/UI/Component/My eID/MyEidView.swift b/RIADigiDoc/UI/Component/My eID/MyEidView.swift index a4a9aa82..7306bf23 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidView.swift @@ -130,6 +130,7 @@ struct MyEidView: View { documentExpirationStatus: viewModel .getDocumentExpirationStatus(expiryDate: idCardData.publicData.dateOfExpiry) ) + .padding(.top, Dimensions.Padding.SPadding) } else if selectedTab == .pinsAndCertificates { MyEidPinsAndCertificatesView( isPin1Blocked: $isPin1Blocked, @@ -141,8 +142,10 @@ struct MyEidView: View { signCertValidTo: idCardData.signCertNotValidDate ?? "", isPUKChangeable: idCardData.isPUKChangeable ) + .padding(.top, Dimensions.Padding.SPadding) } } + .padding(.top, Dimensions.Padding.SPadding) .accessibilityFocused($isTabFocused) .onAppear { DispatchQueue.main.async { diff --git a/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift b/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift index dbb6fc58..24640949 100644 --- a/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift +++ b/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift @@ -204,8 +204,11 @@ struct RecentDocumentsView: View { } .onChange(of: isNavigatingToEncryptView) { _, newValue in if newValue { - pathManager.navigate(to: .encryptView(isWithEncryption: false)) - isNavigatingToEncryptView = false + Task { @MainActor in + let cdocOption = await Container.shared.dataStore().getEncryptionCdocOption(false) + pathManager.navigate(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .files)) + isNavigatingToEncryptView = false + } } } } diff --git a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift index bd2cee94..95c46004 100644 --- a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift +++ b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift @@ -50,6 +50,7 @@ struct FloatingLabelTextField: View { let sortPriority: Double let spellOutCharacters: Bool let showBorder: Bool + let accessibilityHint: String let onDone: (() -> Void) init( @@ -69,6 +70,7 @@ struct FloatingLabelTextField: View { sortPriority: Double = 0, spellOutCharacters: Bool = false, showBorder: Bool = true, + accessibilityHint: String = "", onDone: @escaping (() -> Void) = {} ) { self.title = title @@ -87,6 +89,7 @@ struct FloatingLabelTextField: View { self.sortPriority = sortPriority self.spellOutCharacters = spellOutCharacters self.showBorder = showBorder + self.accessibilityHint = accessibilityHint self.onDone = onDone } @@ -174,6 +177,14 @@ struct FloatingLabelTextField: View { .joined(separator: " ") } + private var fieldAccessibilityValue: String { + if text.isEmpty { return "" } + if isSecure && !isPasswordVisible { + return String(repeating: "•", count: text.count) + } + return text + } + private var shouldShowToolbar: Bool { fieldIsFocused && (showDashButton || keyboardType.needsDoneButton) } @@ -359,7 +370,9 @@ struct FloatingLabelTextField: View { .onChange(of: errorText, { _, newValue in AccessibilityUtil.announceMessage(newValue) }) - .frame(height: Dimensions.Icon.IconSizeXXS) + .accessibilityHint(Text(verbatim: accessibilityHint)) + .accessibilityValue(Text(verbatim: fieldAccessibilityValue)) + .frame(minHeight: Dimensions.Icon.IconSizeXXS) } @ToolbarContentBuilder @@ -523,7 +536,7 @@ private extension View { .disabled(isDisabled) .keyboardType(keyboardType) .submitLabel(submitLabel) - .textContentType(.none) + .textContentType(.init(rawValue: "")) .autocorrectionDisabled(true) .textInputAutocapitalization(.never) .speechSpellsOutCharacters(spellOut) diff --git a/RIADigiDoc/UI/Component/Shared/TabView.swift b/RIADigiDoc/UI/Component/Shared/TabView.swift index 1b474713..326591b5 100644 --- a/RIADigiDoc/UI/Component/Shared/TabView.swift +++ b/RIADigiDoc/UI/Component/Shared/TabView.swift @@ -58,9 +58,9 @@ struct TabView: View where Tab.RawValue == .foregroundStyle(isSelected ? theme.primary : theme.onSurface) .multilineTextAlignment(.center) .lineLimit(nil) - .minimumScaleFactor(0.9) - .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame(maxWidth: .infinity) .padding(.horizontal, Dimensions.Padding.XSPadding) + .padding(.vertical, Dimensions.Padding.XSPadding) .accessibilityLabel(Text(verbatim: "\(title), " + "\(languageSettings.localized("Tab")) \(index + 1) / \(titles.count), " + @@ -72,22 +72,20 @@ struct TabView: View where Tab.RawValue == } .buttonStyle(.plain) .frame(maxWidth: .infinity, minHeight: tabMinHeight) - .overlay(alignment: .bottom) { + .accessibilityLabel(titles[index].lowercased()) + } + } + .fixedSize(horizontal: false, vertical: true) + .overlay(alignment: .bottom) { + HStack(spacing: Dimensions.Padding.ZeroPadding) { + ForEach(titles.indices, id: \.self) { index in Rectangle() - .fill(theme.outlineVariant) + .fill(selectedIndex.wrappedValue == index ? theme.primary : theme.outlineVariant) .frame(height: Dimensions.Height.SBorder) } - .overlay(alignment: .bottom) { - if isSelected { - Rectangle() - .fill(theme.primary) - .frame(height: Dimensions.Height.SBorder) - } - } - .accessibilityLabel(titles[index].lowercased()) } } - .padding(.top, Dimensions.Padding.LPadding) + content() } } diff --git a/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift b/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift index 1b5b5c42..f6a3a0a0 100644 --- a/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift +++ b/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift @@ -55,6 +55,7 @@ struct SigningServicesSettingsView: View { } } ) + .padding(.top, Dimensions.Padding.MPadding) } } } diff --git a/RIADigiDoc/UI/Navigation/NavigationDestinations.swift b/RIADigiDoc/UI/Navigation/NavigationDestinations.swift index 6b25bc1e..a4f2cd38 100644 --- a/RIADigiDoc/UI/Navigation/NavigationDestinations.swift +++ b/RIADigiDoc/UI/Navigation/NavigationDestinations.swift @@ -40,8 +40,8 @@ struct NavigationDestinations: ViewModifier { case .recentDocumentsView(let folderURL, let extensions): RecentDocumentsView(folderURL: folderURL, extensions: extensions) - case .encryptRecipientView: - EncryptRecipientView() + case .encryptRecipientView(let cdocOption): + EncryptRecipientView(cdocOption: cdocOption) case .signingView: SigningView() @@ -72,10 +72,14 @@ struct NavigationDestinations: ViewModifier { ActionMethodSelectionView(actionType: actionType, methods: methods) case .encryptView( - let isWithEncryption + let isWithEncryption, + let cdocOption, + let selectedTab ): EncryptView( isWithEncryption: isWithEncryption, + cdocOption: cdocOption, + selectedTab: selectedTab ) case .recipientDetailView( let recipient, diff --git a/RIADigiDoc/Util/Recipient/RecipientUtil.swift b/RIADigiDoc/Util/Recipient/RecipientUtil.swift index c6aed6a6..e53acf84 100644 --- a/RIADigiDoc/Util/Recipient/RecipientUtil.swift +++ b/RIADigiDoc/Util/Recipient/RecipientUtil.swift @@ -37,6 +37,8 @@ struct RecipientUtil: RecipientUtilProtocol { return "Smart-ID" case .eSealType: return "Certificate for Encryption" + case .passwordType: + return "Password" } } }