From 4271f170bac1ebf4962ec2d04233c5a2db25d86f Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Wed, 8 Apr 2026 14:22:11 +0300 Subject: [PATCH 1/6] Add accessibility improvements to Crypto views --- .../Crypto/CryptoDataFilesLockedSection.swift | 4 +- .../Container/Crypto/EncryptView.swift | 24 ++++++--- .../Recipient/EncryptRecipientView.swift | 53 +++++++++++++++++-- .../Crypto/Recipient/RecipientsView.swift | 12 ++--- .../Container/Signing/NFC/NFCView.swift | 4 ++ RIADigiDoc/UI/Component/HomeView.swift | 1 + RIADigiDoc/ViewModel/EncryptViewModel.swift | 11 +++- RIADigiDoc/ViewModel/SigningViewModel.swift | 9 ++++ 8 files changed, 95 insertions(+), 23 deletions(-) diff --git a/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesLockedSection.swift b/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesLockedSection.swift index c6bb994e..03131208 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesLockedSection.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesLockedSection.swift @@ -40,8 +40,8 @@ struct CryptoDataFilesLockedSection: View { Text(verbatim: languageSettings.localized("Crypto files encrypted")) .font(typography.bodyMedium) .fixedSize(horizontal: false, vertical: true) - .lineLimit(4) - .truncationMode(.middle) + .lineLimit(nil) + .minimumScaleFactor(0.5) .multilineTextAlignment(TextAlignment.leading) .accessibilityLabel( Text( diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index cc4bd793..410b3e9d 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -25,6 +25,7 @@ import UtilsLib struct EncryptView: View { @Environment(\.presentationMode) var presentationMode + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @AppTheme private var theme @AppTypography private var typography @Environment(NavigationPathManager.self) private var pathManager @@ -513,18 +514,27 @@ struct EncryptView: View { .animation(.easeInOut, value: showRenameModal) .onChange(of: viewModel.errorMessage) { _, error in guard let error else { return } - Toast.show( - languageSettings.localized(error.key, [error.args.joined(separator: ", ")]) - ) + let localizedMessage = languageSettings + .localized(error.key, [error.args.joined(separator: ", ")]) + Toast.show(localizedMessage) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(localizedMessage) + } + viewModel.resetErrorMessage() encryptionButtonEnabled = true } .onChange(of: viewModel.successMessage) { _, message in guard let message else { return } - Toast.show( - languageSettings.localized(message.key, [message.args.joined(separator: ", ")]), - type: .success - ) + let localizedMessage = languageSettings + .localized(message.key, [message.args.joined(separator: ", ")]) + Toast.show(localizedMessage, type: .success) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(localizedMessage) + } + viewModel.resetSuccessMessage() } .onChange(of: viewModel.navigateToNestedSignedContainerView) { _, isNavigating in diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift index dfa93077..02064236 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift @@ -24,6 +24,7 @@ import CommonsLib import CryptoObjCWrapper struct EncryptRecipientView: View { + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @Environment(\.dismiss) private var dismiss @Environment(LanguageSettings.self) private var languageSettings @@ -32,6 +33,9 @@ struct EncryptRecipientView: View { @AppTheme private var theme @AppTypography private var typography + @AccessibilityFocusState private var isTitleFocused: Bool + @AccessibilityFocusState private var isSearchFieldFocused: Bool + @AccessibilityFocusState private var focusedRecipientIndex: Int? @FocusState private var isSearchFocused: Bool @State private var isSearchExpanded = false @@ -134,8 +138,16 @@ struct EncryptRecipientView: View { .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 + } + } } HStack { Image(systemName: "magnifyingglass") @@ -149,6 +161,7 @@ struct EncryptRecipientView: View { .submitLabel(.done) .textInputAutocapitalization(.never) .autocorrectionDisabled(true) + .accessibilityFocused($isSearchFieldFocused) .focused($isSearchFocused) .onChange(of: isSearchFocused) { _, newValue in isSearchExpanded = newValue @@ -156,24 +169,48 @@ struct EncryptRecipientView: View { .onChange(of: viewModel.searchText) { viewModel.handleSearchTextChange() } + .onChange(of: filteredRecipients) { _, newValue in + guard !newValue.isEmpty else { return } + + Task { @MainActor in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + focusedRecipientIndex = 0 + } + } + } .onSubmit { if viewModel.searchText.allSatisfy(\.isNumber) && viewModel.searchText.count == 11 && !PersonalCodeValidator.isPersonalCodeValid(viewModel.searchText) { - Toast.show(languageSettings.localized("Personal code is not valid")) + let personalCodeNotValidMessage = languageSettings.localized( + "Personal code is not valid" + ) + + Toast.show(personalCodeNotValidMessage) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage( + personalCodeNotValidMessage + ) + } return } Task { await viewModel.loadRecipients() - if noRecipients { - showNoRecipientsFoundMessage = true - } + await MainActor.run { + isTitleFocused = false - isSearchFocused = true + if voiceOverEnabled && noRecipients { + AccessibilityUtil.announceMessage( + languageSettings.localized("No recipients found") + ) + } + } } } + .accessibilitySortPriority(1) if isSearchExpanded { Button( @@ -204,6 +241,7 @@ struct EncryptRecipientView: View { ) .padding(.top, Dimensions.Padding.LPadding) .padding(.bottom, Dimensions.Padding.SPadding) + ScrollView { if noSearchResults && !isSearchExpanded { VStack { @@ -229,8 +267,10 @@ struct EncryptRecipientView: View { addedRecipientsSection } } + .accessibilitySortPriority(filteredRecipients.isEmpty ? 2 : 0) } .padding(.horizontal, Dimensions.Padding.SPadding) + .accessibilityElement(children: .contain) if showRemoveRecipientModal { ConfirmModalView( @@ -298,6 +338,8 @@ struct EncryptRecipientView: View { .padding(Dimensions.Padding.MPadding) .accessibilityElement(children: .ignore) .accessibilityLabel(encryptLabel.lowercased()) + .accessibilityAddTraits([.isButton]) + .accessibilityIdentifier("bottomEncryptButton") } .onAppear { @@ -338,6 +380,7 @@ struct EncryptRecipientView: View { .contentShape(Rectangle()) .buttonStyle(.plain) .background(theme.surface) + .accessibilityFocused($focusedRecipientIndex, equals: index) } private func addedRecipientRow(index: Int, item: Addressee) -> some View { diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/RecipientsView.swift b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/RecipientsView.swift index 531adfcb..5c5f9c52 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/RecipientsView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/RecipientsView.swift @@ -108,13 +108,9 @@ struct RecipientsView: View { .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.leading) .accessibilityLabel({ - if recipientIndex != 0 { - let prefix = languageSettings.localized("Recipient") - let name = nameText.lowercased() - return Text(verbatim: "\(prefix) \(recipientIndex), \(name)") - } else { - return Text(verbatim: nameText.lowercased()) - } + let prefix = languageSettings.localized("Recipient") + let name = nameText.lowercased() + return Text(verbatim: "\(prefix) \(recipientIndex + 1), \(name)") }()) let certType = recipientUtil.getRecipientCertTypeText(certType: recipient.certType) @@ -142,7 +138,7 @@ struct RecipientsView: View { .frame(width: Dimensions.Icon.IconSizeXXS, height: Dimensions.Icon.IconSizeXXS) .foregroundStyle(theme.onSurface) .accessibilityLabel( - Text(verbatim: languageSettings.localized("Remove container")) + Text(verbatim: languageSettings.localized("Remove recipient")) ) }) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift index b97170fa..bcdf1d0a 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift @@ -367,6 +367,10 @@ struct NFCView: View { Toast.show(signatureAddedMessage, type: .success) + if voiceOverEnabled { + AccessibilityUtil.announceMessage(signatureAddedMessage) + } + onSuccess(container) dismiss() } diff --git a/RIADigiDoc/UI/Component/HomeView.swift b/RIADigiDoc/UI/Component/HomeView.swift index 53726e44..8c28a925 100644 --- a/RIADigiDoc/UI/Component/HomeView.swift +++ b/RIADigiDoc/UI/Component/HomeView.swift @@ -303,6 +303,7 @@ struct HomeView: View { if !files.isEmpty { isFileOpeningLoading = true viewModel.isImporting = false + viewModel.setFileOpeningMethod(.all) viewModel.setChosenFiles(.success(files)) } } diff --git a/RIADigiDoc/ViewModel/EncryptViewModel.swift b/RIADigiDoc/ViewModel/EncryptViewModel.swift index e4929cd8..2139bbc9 100644 --- a/RIADigiDoc/ViewModel/EncryptViewModel.swift +++ b/RIADigiDoc/ViewModel/EncryptViewModel.swift @@ -723,10 +723,19 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { isSivaConfirmed: true ) + let signedContainerName = URL(fileURLWithPath: containerName) + .deletingPathExtension() + .appendingPathExtension(Constants.Extension.Default) + .lastPathComponent + + let renamedContainer = try await signedContainer.renameContainer( + to: signedContainerName + ) + sharedContainerViewModel.setCryptoContainer(nil) sharedContainerViewModel.clearContainers() sharedContainerViewModel.setAddedFilesCount(addedFiles: dataFileURLs.count) - sharedContainerViewModel.setSignedContainer(signedContainer) + sharedContainerViewModel.setSignedContainer(renamedContainer) return true } diff --git a/RIADigiDoc/ViewModel/SigningViewModel.swift b/RIADigiDoc/ViewModel/SigningViewModel.swift index f9f1347e..3096c174 100644 --- a/RIADigiDoc/ViewModel/SigningViewModel.swift +++ b/RIADigiDoc/ViewModel/SigningViewModel.swift @@ -609,6 +609,15 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { let cryptoContainer = try await CryptoContainer.openOrCreate(dataFiles: dataFileURLs) + let cryptoContainerName = URL(fileURLWithPath: containerName) + .deletingPathExtension() + .appendingPathExtension(Constants.Extension.DefaultCrypto) + .lastPathComponent + + try await cryptoContainer.renameContainer( + to: cryptoContainerName + ) + sharedContainerViewModel.setSignedContainer(nil) sharedContainerViewModel.clearContainers() sharedContainerViewModel.setAddedFilesCount(addedFiles: dataFileURLs.count) From 8dad37d60e1eae7f889b9d76916e5d9fad06ffda Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Wed, 8 Apr 2026 14:52:49 +0300 Subject: [PATCH 2/6] Message announcement fixes --- .../Container/Crypto/DecryptRootView.swift | 17 +++++++++-- .../Container/Signing/IdCard/IdCardView.swift | 28 +++++++++++++++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/RIADigiDoc/UI/Component/Container/Crypto/DecryptRootView.swift b/RIADigiDoc/UI/Component/Container/Crypto/DecryptRootView.swift index 4f630d5a..0b652971 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/DecryptRootView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/DecryptRootView.swift @@ -24,6 +24,7 @@ import IdCardLib import CommonsLib struct DecryptRootView: View { + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @Environment(\.dismiss) private var dismiss @Environment(LanguageSettings.self) private var languageSettings @Environment(NavigationPathManager.self) private var pathManager @@ -36,6 +37,10 @@ struct DecryptRootView: View { private let sharedContainerViewModel: SharedContainerViewModelProtocol + private var containerSuccessfullyDecryptedMessage: String { + languageSettings.localized("Container successfully decrypted") + } + init() { _viewModel = State(wrappedValue: Container.shared.decryptRootViewModel()) self.sharedContainerViewModel = Container.shared.sharedContainerViewModel() @@ -60,8 +65,12 @@ struct DecryptRootView: View { sharedContainerViewModel.setCryptoContainer(container) Toast.show(languageSettings.localized( - "Container successfully decrypted" + containerSuccessfullyDecryptedMessage ), type: .success) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(containerSuccessfullyDecryptedMessage) + } } ) } @@ -79,8 +88,12 @@ struct DecryptRootView: View { sharedContainerViewModel.setCryptoContainer(container) Toast.show(languageSettings.localized( - "Container successfully decrypted" + containerSuccessfullyDecryptedMessage ), type: .success) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(containerSuccessfullyDecryptedMessage) + } } ) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift index cea163c2..ed715878 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift @@ -121,6 +121,10 @@ struct IdCardView: View { languageSettings.localized("Signature added") } + private var generalErrorMessage: String { + languageSettings.localized("General error") + } + init( actionType: ActionType, actionMethods: [ActionMethod], @@ -187,6 +191,11 @@ struct IdCardView: View { await MainActor.run { Toast.show(errorMessage) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(generalErrorMessage) + } + viewModel.resetErrors() resetIdCardAction() if shouldDismiss { @@ -394,6 +403,11 @@ struct IdCardView: View { await viewModel.stopDiscoveringReaders() await MainActor.run { Toast.show(errorMessage) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(errorMessage) + } + viewModel.resetErrors() resetIdCardAction() @@ -436,9 +450,12 @@ struct IdCardView: View { resetIdCardAction() guard let container = signedContainer else { - Toast.show( - languageSettings.localized("General error") - ) + Toast.show(generalErrorMessage) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(generalErrorMessage) + } + return } @@ -467,6 +484,11 @@ struct IdCardView: View { await MainActor.run { Toast.show(errorMessage) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(generalErrorMessage) + } + viewModel.resetErrors() resetIdCardAction() if shouldDismiss { From 8a3e13aafd4ef27650fc8ad98b9585fbc74821eb Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Wed, 8 Apr 2026 17:31:14 +0300 Subject: [PATCH 3/6] Fix announcing error messages --- RIADigiDoc/Domain/Model/Error/NFC/DecryptError.swift | 3 +++ RIADigiDoc/Domain/NFC/NFCOperationBase.swift | 1 + RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift | 6 ++++++ RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift | 3 +++ 4 files changed, 13 insertions(+) diff --git a/RIADigiDoc/Domain/Model/Error/NFC/DecryptError.swift b/RIADigiDoc/Domain/Model/Error/NFC/DecryptError.swift index 52a0df51..efc859c2 100644 --- a/RIADigiDoc/Domain/Model/Error/NFC/DecryptError.swift +++ b/RIADigiDoc/Domain/Model/Error/NFC/DecryptError.swift @@ -22,6 +22,7 @@ import Foundation public enum DecryptError: Error { case containerFileInvalid case recipientsEmpty + case noCertLock case cancelled case unknown(Error) } @@ -33,6 +34,8 @@ extension DecryptError: LocalizedError { return "Container file is invalid" case .recipientsEmpty: return "Person or company does not own a valid certificate" + case .noCertLock: + return "Failed to find lock for cert" case .cancelled: return "Operation cancelled by user" case .unknown(let error): diff --git a/RIADigiDoc/Domain/NFC/NFCOperationBase.swift b/RIADigiDoc/Domain/NFC/NFCOperationBase.swift index a310960c..d99f01ca 100644 --- a/RIADigiDoc/Domain/NFC/NFCOperationBase.swift +++ b/RIADigiDoc/Domain/NFC/NFCOperationBase.swift @@ -85,6 +85,7 @@ public class NFCOperationBase: NSObject, Loggable, @MainActor NFCTagReaderSessio ) { Self.logger().error("NFC: Failed to find lock for cert") nfcError = strings?.wrongCardErrorMessage ?? "" + operationError = DecryptError.noCertLock session.invalidate(errorMessage: nfcError) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift index bcdf1d0a..c0408cdc 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift @@ -257,6 +257,12 @@ struct NFCView: View { .onChange(of: viewModel.nfcErrorKey) { _, newKey in guard newKey != nil else { return } Toast.show(nfcErrorMessage) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(nfcErrorMessage) + } + + viewModel.resetErrors() } .onDisappear { cancelSigning() diff --git a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift index c03aa9a7..eea8e433 100644 --- a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift @@ -344,6 +344,9 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { case .containerFileInvalid, .recipientsEmpty: NFCViewModel.logger().error("NFC: Configuration error") nfcErrorKey = "NFC session error" + case .noCertLock: + NFCViewModel.logger().error("NFC: Failed to find lock for cert") + nfcErrorKey = "Failed to find lock for cert" case .unknown(let underlying): NFCViewModel.logger().error("NFC: Unknown error - \(underlying)") nfcErrorKey = "General error" From 268fc279bee1b8604918d7f70e69a729e680d4d4 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Wed, 8 Apr 2026 18:11:58 +0300 Subject: [PATCH 4/6] Code improvements --- .../Container/Crypto/DecryptRootView.swift | 26 ++++++------- .../Container/Signing/IdCard/IdCardView.swift | 39 +++++++------------ 2 files changed, 25 insertions(+), 40 deletions(-) diff --git a/RIADigiDoc/UI/Component/Container/Crypto/DecryptRootView.swift b/RIADigiDoc/UI/Component/Container/Crypto/DecryptRootView.swift index 0b652971..00964475 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/DecryptRootView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/DecryptRootView.swift @@ -64,13 +64,7 @@ struct DecryptRootView: View { sharedContainerViewModel.removeLastContainer() sharedContainerViewModel.setCryptoContainer(container) - Toast.show(languageSettings.localized( - containerSuccessfullyDecryptedMessage - ), type: .success) - - if voiceOverEnabled { - AccessibilityUtil.announceMessage(containerSuccessfullyDecryptedMessage) - } + showContainerSuccessfullyDecryptedMessage() } ) } @@ -87,13 +81,7 @@ struct DecryptRootView: View { sharedContainerViewModel.removeLastContainer() sharedContainerViewModel.setCryptoContainer(container) - Toast.show(languageSettings.localized( - containerSuccessfullyDecryptedMessage - ), type: .success) - - if voiceOverEnabled { - AccessibilityUtil.announceMessage(containerSuccessfullyDecryptedMessage) - } + showContainerSuccessfullyDecryptedMessage() } ) } @@ -109,6 +97,16 @@ struct DecryptRootView: View { } } } + + func showContainerSuccessfullyDecryptedMessage() { + Toast.show(languageSettings.localized( + containerSuccessfullyDecryptedMessage + ), type: .success) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(containerSuccessfullyDecryptedMessage) + } + } } #Preview { diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift index ed715878..040eba32 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift @@ -190,11 +190,7 @@ struct IdCardView: View { } await MainActor.run { - Toast.show(errorMessage) - - if voiceOverEnabled { - AccessibilityUtil.announceMessage(generalErrorMessage) - } + showMessage(message: errorMessage, type: .error) viewModel.resetErrors() resetIdCardAction() @@ -402,11 +398,7 @@ struct IdCardView: View { private func handleCardError() async { await viewModel.stopDiscoveringReaders() await MainActor.run { - Toast.show(errorMessage) - - if voiceOverEnabled { - AccessibilityUtil.announceMessage(errorMessage) - } + showMessage(message: errorMessage, type: .error) viewModel.resetErrors() resetIdCardAction() @@ -450,12 +442,7 @@ struct IdCardView: View { resetIdCardAction() guard let container = signedContainer else { - Toast.show(generalErrorMessage) - - if voiceOverEnabled { - AccessibilityUtil.announceMessage(generalErrorMessage) - } - + showMessage(message: generalErrorMessage, type: .error) return } @@ -483,12 +470,7 @@ struct IdCardView: View { } await MainActor.run { - Toast.show(errorMessage) - - if voiceOverEnabled { - AccessibilityUtil.announceMessage(generalErrorMessage) - } - + showMessage(message: errorMessage, type: .error) viewModel.resetErrors() resetIdCardAction() if shouldDismiss { @@ -517,15 +499,20 @@ struct IdCardView: View { isShowingPinView = false isShowingLoadingView = false - Toast.show(signatureAddedMessage, type: .success) - if voiceOverEnabled { - AccessibilityUtil.announceMessage(signatureAddedMessage) - } + showMessage(message: signatureAddedMessage, type: .success) onSuccess(container) dismiss() } } + + private func showMessage(message: String, type: ToastType) { + Toast.show(message, type: type) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(message) + } + } } #Preview { From d4581d341c286e2724f801901243c1bbe9588dce Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Thu, 9 Apr 2026 15:44:05 +0300 Subject: [PATCH 5/6] Fix showing keyboard in rename modal view --- .../Container/Signing/Modal/RenameModalView.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/RIADigiDoc/UI/Component/Container/Signing/Modal/RenameModalView.swift b/RIADigiDoc/UI/Component/Container/Signing/Modal/RenameModalView.swift index 5b575d33..9bacbcd4 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/Modal/RenameModalView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/Modal/RenameModalView.swift @@ -26,6 +26,7 @@ struct RenameModalView: View { @AppTypography private var typography @State private var containerName: String + @State private var keyboardHeight: CGFloat = 0 let onConfirm: (String) -> Void let onCancel: () -> Void @@ -62,6 +63,18 @@ struct RenameModalView: View { onConfirm: { onConfirm(containerName) }, onCancel: onCancel ) + .offset(y: -keyboardHeight / 2) + .animation(.easeOut(duration: Dimensions.Duration.focusAnimation), value: keyboardHeight) + } + .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 } } } From 8a0015f38799d0b90401854741b8565e506ac500 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Thu, 9 Apr 2026 15:44:13 +0300 Subject: [PATCH 6/6] Swiftlint fixes --- RIADigiDoc/Domain/NFC/NFCOperationBase.swift | 6 +++--- .../Domain/NFC/OperationReadCertAndSign.swift | 1 + .../Component/Container/Crypto/EncryptView.swift | 2 +- .../Crypto/Recipient/EncryptRecipientView.swift | 16 ++++++++++------ .../Signing/MobileId/MobileIdView.swift | 2 +- .../Container/Signing/NFC/NFCView.swift | 4 ++-- .../Shared/FloatingLabelTextField.swift | 3 ++- .../Shared/Preview/PreviewController.swift | 2 ++ RIADigiDoc/UI/Component/Toast/ToastQueue.swift | 2 +- RIADigiDoc/ViewModel/DiagnosticsViewModel.swift | 4 ++-- RIADigiDoc/ViewModel/EncryptViewModel.swift | 4 +++- .../Signing/IdCard/IdCardViewModel.swift | 6 +++--- 12 files changed, 31 insertions(+), 21 deletions(-) diff --git a/RIADigiDoc/Domain/NFC/NFCOperationBase.swift b/RIADigiDoc/Domain/NFC/NFCOperationBase.swift index d99f01ca..1f003d44 100644 --- a/RIADigiDoc/Domain/NFC/NFCOperationBase.swift +++ b/RIADigiDoc/Domain/NFC/NFCOperationBase.swift @@ -78,17 +78,17 @@ public class NFCOperationBase: NSObject, Loggable, @MainActor NFCTagReaderSessio nfcError = strings?.technicalErrorMessage ?? "" } } - + func handleNoCertLockError( error: Error, session: NFCTagReaderSession ) { - Self.logger().error("NFC: Failed to find lock for cert") + Self.logger().error("NFC: Failed to find lock for cert, error: \(error)") nfcError = strings?.wrongCardErrorMessage ?? "" operationError = DecryptError.noCertLock session.invalidate(errorMessage: nfcError) } - + func handleIdCardInternalError( _ error: IdCardInternalError, session: NFCTagReaderSession diff --git a/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift b/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift index 07fc335e..e8c55533 100644 --- a/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift +++ b/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift @@ -75,6 +75,7 @@ public class OperationReadCertAndSign: NFCOperationBase, OperationReadCertAndSig // MARK: - NFCTagReaderSessionDelegate + // swiftlint:disable:next cyclomatic_complexity public override func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { Task { @MainActor in defer { diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index 410b3e9d..cafd2b14 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -159,7 +159,7 @@ struct EncryptView: View { private var containerExtension: String { URL(fileURLWithPath: viewModel.containerName).pathExtension } - + private var containerIcon: String { viewModel.isContainerDecrypted ? "ic_m3_encrypted_off_48pt_wght400" diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift index 02064236..74c59006 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift @@ -71,11 +71,11 @@ struct EncryptRecipientView: View { var encryptLabel: String { languageSettings.localized("Encrypt") } - + var noSearchResultsMessage: String { languageSettings.localized("Person or company does not own a valid certificate") } - + private var addedRecipientsSection: some View { VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { if noSearchResults { @@ -103,7 +103,7 @@ struct EncryptRecipientView: View { .listRowSpacing(0) .listSectionSpacing(.compact) } - + private var filteredRecipientsSection: some View { VStack { if #available(iOS 26.0, *) { @@ -256,7 +256,11 @@ struct EncryptRecipientView: View { .scrollDisabled(true) .scrollContentBackground(.hidden) } else if showNoRecipientsFoundMessage { - emptyStateView(languageSettings.localized("Person or company does not own a valid certificate")) + emptyStateView( + languageSettings.localized( + "Person or company does not own a valid certificate" + ) + ) } else { filteredRecipientsSection } @@ -339,7 +343,7 @@ struct EncryptRecipientView: View { .accessibilityElement(children: .ignore) .accessibilityLabel(encryptLabel.lowercased()) .accessibilityAddTraits([.isButton]) - + .accessibilityIdentifier("bottomEncryptButton") } .onAppear { @@ -407,7 +411,7 @@ struct EncryptRecipientView: View { .buttonStyle(.plain) .background(theme.surface) } - + private func emptyStateView(_ text: String) -> some View { ContentUnavailableView { Text(verbatim: text) diff --git a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift index 5a9f2c87..e0c7a5a8 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift @@ -152,7 +152,7 @@ struct MobileIdView: View { !messageKey.isEmpty { let extraArguments = viewModel.mobileIdAlertMessageExtraArguments let message = languageSettings.localized(messageKey, extraArguments) - + Toast.show(message) if voiceOverEnabled { AccessibilityUtil.announceMessage(message) diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift index c0408cdc..80bf779a 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift @@ -97,7 +97,7 @@ struct NFCView: View { languageSettings.localized(key, args) } } - + private var signatureAddedMessage: String { languageSettings.localized("Signature added") } @@ -370,7 +370,7 @@ struct NFCView: View { guard let container = updatedContainer else { return } - + Toast.show(signatureAddedMessage, type: .success) if voiceOverEnabled { diff --git a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift index dd41b70a..97018b6c 100644 --- a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift +++ b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift @@ -29,7 +29,7 @@ struct FloatingLabelTextField: View { @AccessibilityFocusState private var isAccessibilityFocused: Bool - @State private var selection: TextSelection? = nil + @State private var selection: TextSelection? @State private var floatingLabelHeight: CGFloat = 0 @@ -491,6 +491,7 @@ struct FloatingLabelTextField: View { } private extension View { + // swiftlint:disable:next function_parameter_count func textFieldModifiers( isDisabled: Bool, keyboardType: UIKeyboardType, diff --git a/RIADigiDoc/UI/Component/Shared/Preview/PreviewController.swift b/RIADigiDoc/UI/Component/Shared/Preview/PreviewController.swift index e4b650f9..ddde1ba1 100644 --- a/RIADigiDoc/UI/Component/Shared/Preview/PreviewController.swift +++ b/RIADigiDoc/UI/Component/Shared/Preview/PreviewController.swift @@ -20,6 +20,7 @@ import SwiftUI import QuickLook +// swiftlint:disable unused_parameter struct PreviewController: UIViewControllerRepresentable { let url: URL @Binding var isPresented: Bool @@ -109,3 +110,4 @@ class PreviewQLController: QLPreviewController { super.pressesBegan(presses, with: event) } } +// swiftlint:enable unused_parameter diff --git a/RIADigiDoc/UI/Component/Toast/ToastQueue.swift b/RIADigiDoc/UI/Component/Toast/ToastQueue.swift index 060b9fc7..1a0d81c8 100644 --- a/RIADigiDoc/UI/Component/Toast/ToastQueue.swift +++ b/RIADigiDoc/UI/Component/Toast/ToastQueue.swift @@ -24,7 +24,7 @@ actor ToastQueue { private var queue: [ToastItem] = [] private var isPresenting = false - private var currentMessage: String? = nil + private var currentMessage: String? func enqueue(message: String, duration: TimeInterval, type: ToastType) { // Avoid consecutive duplicate messages diff --git a/RIADigiDoc/ViewModel/DiagnosticsViewModel.swift b/RIADigiDoc/ViewModel/DiagnosticsViewModel.swift index 4ab410f3..09f883c3 100644 --- a/RIADigiDoc/ViewModel/DiagnosticsViewModel.swift +++ b/RIADigiDoc/ViewModel/DiagnosticsViewModel.swift @@ -114,11 +114,11 @@ class DiagnosticsViewModel: DiagnosticsViewModelProtocol, Loggable { func getRpUuid() async -> String { await dataStore.getRelyingPartyUUID() } - + func getTsaUrl() async -> String { await dataStore.getTSAUrl() } - + func getSivaUrl() async -> String { await dataStore.getValidationServiceURL() } diff --git a/RIADigiDoc/ViewModel/EncryptViewModel.swift b/RIADigiDoc/ViewModel/EncryptViewModel.swift index 2139bbc9..20f5a3d1 100644 --- a/RIADigiDoc/ViewModel/EncryptViewModel.swift +++ b/RIADigiDoc/ViewModel/EncryptViewModel.swift @@ -780,7 +780,9 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { ) } - private func getEffectiveContainer(parentContainer: SignedContainerProtocol) async throws -> SignedContainerProtocol { + private func getEffectiveContainer( + parentContainer: SignedContainerProtocol + ) async throws -> SignedContainerProtocol { let isTimestamped = await sivaRepository.isTimestampedContainer(signedContainer: parentContainer) let isCades = await parentContainer.isCades() let isXades = await parentContainer.isXades() diff --git a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift index bb9919f8..6b037c37 100644 --- a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift @@ -111,7 +111,7 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { return container } catch { IdCardViewModel.logger().error("ID-CARD: Unable to decrypt container with ID-card reader. \(error)") - + let nsError = error as NSError if nsError.localizedDescription == "Failed to find lock for cert" { IdCardViewModel.logger().error("ID-CARD: Failed to find lock for cert") @@ -120,7 +120,7 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { shouldDismissForError = true return nil } - + guard let exception = error as? IdCardInternalError else { IdCardViewModel.logger().error("ID-CARD: ID Card General error.") errorMessage = "General error" @@ -214,7 +214,7 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { if pinResponse.pin2RetryCount == 0 { throw IdCardInternalError.remainingPinRetryCount(0) } - + if !pinResponse.pin2Active { throw IdCardInternalError.pinLocked }