From 29287d633eff146e970014525f3f4a40afb6af94 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Mon, 7 Sep 2026 18:05:12 +0300 Subject: [PATCH] Fix opening external container when one is already open --- RIADigiDoc/DI/AppContainer.swift | 3 +- .../Container/Signing/SigningRootView.swift | 12 +- RIADigiDoc/UI/Component/HomeView.swift | 19 +++- .../Presentation/ModalPresentationUtil.swift | 35 ++++++ RIADigiDoc/ViewModel/HomeViewModel.swift | 6 + .../Protocols/HomeViewModelProtocol.swift | 1 + .../RootView/SigningRootViewModel.swift | 26 ++++- .../SigningRootViewModelProtocol.swift | 6 + .../ViewModel/HomeViewModelTests.swift | 67 +++++++++++ .../RootView/SigningRootViewModelTests.swift | 107 ++++++++++++++++++ 10 files changed, 270 insertions(+), 12 deletions(-) create mode 100644 RIADigiDoc/Util/Presentation/ModalPresentationUtil.swift create mode 100644 RIADigiDocTests/ViewModel/Signing/RootView/SigningRootViewModelTests.swift diff --git a/RIADigiDoc/DI/AppContainer.swift b/RIADigiDoc/DI/AppContainer.swift index e73ab849..3170be66 100644 --- a/RIADigiDoc/DI/AppContainer.swift +++ b/RIADigiDoc/DI/AppContainer.swift @@ -481,7 +481,8 @@ extension Container { var signingRootViewModel: Factory { self { @MainActor in SigningRootViewModel( - dataStore: self.dataStore() + dataStore: self.dataStore(), + sharedContainerViewModel: self.sharedContainerViewModel() ) } } diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift index eb760c5e..bfb7700f 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift @@ -60,9 +60,7 @@ struct SigningRootView: View { signedContainer: container, onSuccess: { container in isSuccess = true - sharedContainerViewModel.removeLastContainer() - sharedContainerViewModel.setSignedContainer(container) - sharedContainerViewModel.setIsSignatureAdded(true) + viewModel.applySignedContainer(container, replacing: signedContainer) } ) } @@ -71,9 +69,7 @@ struct SigningRootView: View { MobileIdView( signedContainer: container, onSuccess: { container in - sharedContainerViewModel.removeLastContainer() - sharedContainerViewModel.setSignedContainer(container) - sharedContainerViewModel.setIsSignatureAdded(true) + viewModel.applySignedContainer(container, replacing: signedContainer) } ) } @@ -82,9 +78,7 @@ struct SigningRootView: View { SmartIdView( signedContainer: container, onSuccess: { container in - sharedContainerViewModel.removeLastContainer() - sharedContainerViewModel.setSignedContainer(container) - sharedContainerViewModel.setIsSignatureAdded(true) + viewModel.applySignedContainer(container, replacing: signedContainer) } ) } diff --git a/RIADigiDoc/UI/Component/HomeView.swift b/RIADigiDoc/UI/Component/HomeView.swift index 2e41d79e..fb15cef0 100644 --- a/RIADigiDoc/UI/Component/HomeView.swift +++ b/RIADigiDoc/UI/Component/HomeView.swift @@ -309,7 +309,14 @@ struct HomeView: View { } private func handleFiles(_ files: [URL]) { - if !files.isEmpty { + guard !files.isEmpty else { return } + + dismissHomeModals() + ModalPresentationUtil.dismissPresentedModals() + + DispatchQueue.main.async { + pathManager.popToRoot() + viewModel.closeOpenContainers() isFileOpeningLoading = true viewModel.isImporting = false viewModel.setFileOpeningMethod(.all) @@ -317,6 +324,16 @@ struct HomeView: View { } } + private func dismissHomeModals() { + showFilesBottomSheet = false + showSignatureBottomSheet = false + showCryptoBottomSheet = false + showHomeMenuBottomSheet = false + isAllFilesImporting = false + isSigningImporting = false + isCryptoImporting = false + } + private func handleIncoming(url: URL) { let webEidURL = (WebEidUriUtil.isWebEidUri(url)) ? url : nil diff --git a/RIADigiDoc/Util/Presentation/ModalPresentationUtil.swift b/RIADigiDoc/Util/Presentation/ModalPresentationUtil.swift new file mode 100644 index 00000000..958b174a --- /dev/null +++ b/RIADigiDoc/Util/Presentation/ModalPresentationUtil.swift @@ -0,0 +1,35 @@ +/* + * 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 UIKit + +class ModalPresentationUtil { + + @MainActor + public static func dismissPresentedModals() { + let rootViewControllers = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .compactMap { $0.rootViewController } + + for rootViewController in rootViewControllers where rootViewController.presentedViewController != nil { + rootViewController.dismiss(animated: false) + } + } +} diff --git a/RIADigiDoc/ViewModel/HomeViewModel.swift b/RIADigiDoc/ViewModel/HomeViewModel.swift index 429ff2ca..97211961 100644 --- a/RIADigiDoc/ViewModel/HomeViewModel.swift +++ b/RIADigiDoc/ViewModel/HomeViewModel.swift @@ -63,6 +63,12 @@ class HomeViewModel: HomeViewModelProtocol, Loggable { sharedContainerViewModel.setFileOpeningMethod(method) } + func closeOpenContainers() { + sharedContainerViewModel.setSignedContainer(nil) + sharedContainerViewModel.setCryptoContainer(nil) + sharedContainerViewModel.clearContainers() + } + func getRecentDocumentsFolder() -> URL? { do { return try Directories.getCacheDirectory(fileManager: fileManager) diff --git a/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift index 2dda0192..699be1e3 100644 --- a/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift @@ -28,4 +28,5 @@ public protocol HomeViewModelProtocol: Sendable { func getRecentDocumentsFolder() -> URL? func getSharedFiles() async -> [URL] func setFileOpeningMethod(_ method: FileOpeningMethod) + func closeOpenContainers() } diff --git a/RIADigiDoc/ViewModel/Signing/RootView/SigningRootViewModel.swift b/RIADigiDoc/ViewModel/Signing/RootView/SigningRootViewModel.swift index 97a23ff8..09336ea7 100644 --- a/RIADigiDoc/ViewModel/Signing/RootView/SigningRootViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/RootView/SigningRootViewModel.swift @@ -20,18 +20,42 @@ import UtilsLib import Foundation import Observation +import CommonsLib +import LibdigidocLibSwift @Observable @MainActor class SigningRootViewModel: SigningRootViewModelProtocol, Loggable { private let dataStore: DataStoreProtocol + private let sharedContainerViewModel: SharedContainerViewModelProtocol - init(dataStore: DataStoreProtocol) { + init( + dataStore: DataStoreProtocol, + sharedContainerViewModel: SharedContainerViewModelProtocol + ) { self.dataStore = dataStore + self.sharedContainerViewModel = sharedContainerViewModel } func getSelectedSigningMethod() async -> ActionMethod { return await dataStore.getSelectedSigningMethod() } + + func applySignedContainer( + _ signedContainer: SignedContainerProtocol, + replacing containerBeingSigned: GeneralContainer? + ) { + guard let containerBeingSigned, + sharedContainerViewModel.currentContainer() === containerBeingSigned else { + SigningRootViewModel.logger().info( + "Container was replaced while signing. Dropping the signed container" + ) + return + } + + sharedContainerViewModel.removeLastContainer() + sharedContainerViewModel.setSignedContainer(signedContainer) + sharedContainerViewModel.setIsSignatureAdded(true) + } } diff --git a/RIADigiDoc/ViewModel/Signing/RootView/SigningRootViewModelProtocol.swift b/RIADigiDoc/ViewModel/Signing/RootView/SigningRootViewModelProtocol.swift index 6c75364b..beb75b12 100644 --- a/RIADigiDoc/ViewModel/Signing/RootView/SigningRootViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Signing/RootView/SigningRootViewModelProtocol.swift @@ -18,9 +18,15 @@ */ import Foundation +import CommonsLib +import LibdigidocLibSwift /// @mockable @MainActor public protocol SigningRootViewModelProtocol: Sendable { func getSelectedSigningMethod() async -> ActionMethod + func applySignedContainer( + _ signedContainer: SignedContainerProtocol, + replacing containerBeingSigned: GeneralContainer? + ) } diff --git a/RIADigiDocTests/ViewModel/HomeViewModelTests.swift b/RIADigiDocTests/ViewModel/HomeViewModelTests.swift index cbe638cd..b727a8ab 100644 --- a/RIADigiDocTests/ViewModel/HomeViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/HomeViewModelTests.swift @@ -23,18 +23,21 @@ import CommonsLib import CommonsLibMocks import UtilsLib import UtilsLibMocks +import LibdigidocLibSwift @MainActor struct HomeViewModelTests { private let mockSharedContainerViewModel: SharedContainerViewModelProtocolMock private let mockFileManager: FileManagerProtocolMock private let mockFileUtil: FileUtilProtocolMock + private let mockContainerUtil: ContainerUtilProtocolMock private let viewModel: HomeViewModel init() async throws { mockSharedContainerViewModel = SharedContainerViewModelProtocolMock() mockFileManager = FileManagerProtocolMock() mockFileUtil = FileUtilProtocolMock() + mockContainerUtil = ContainerUtilProtocolMock() viewModel = HomeViewModel( sharedContainerViewModel: mockSharedContainerViewModel, fileManager: mockFileManager, @@ -134,4 +137,68 @@ struct HomeViewModelTests { return } } + + @Test + func closeOpenContainers_clearsTheOpenContainer() async { + let sharedContainerViewModel = SharedContainerViewModel() + let viewModel = makeViewModel(sharedContainerViewModel: sharedContainerViewModel) + sharedContainerViewModel.setSignedContainer(makeSignedContainer()) + + viewModel.closeOpenContainers() + + #expect(sharedContainerViewModel.containers().isEmpty) + #expect(sharedContainerViewModel.currentContainer() == nil) + } + + @Test + func closeOpenContainers_clearsEveryNestedContainer() async { + let sharedContainerViewModel = SharedContainerViewModel() + let viewModel = makeViewModel(sharedContainerViewModel: sharedContainerViewModel) + sharedContainerViewModel.setSignedContainer(makeSignedContainer()) + sharedContainerViewModel.setSignedContainer(makeSignedContainer()) + + #expect(sharedContainerViewModel.containers().count == 2) + + viewModel.closeOpenContainers() + + #expect(sharedContainerViewModel.containers().isEmpty) + #expect(sharedContainerViewModel.currentContainer() == nil) + } + + @Test + func closeOpenContainers_doesNothingWhenNoContainerIsOpen() async { + let sharedContainerViewModel = SharedContainerViewModel() + let viewModel = makeViewModel(sharedContainerViewModel: sharedContainerViewModel) + + viewModel.closeOpenContainers() + + #expect(sharedContainerViewModel.containers().isEmpty) + #expect(sharedContainerViewModel.currentContainer() == nil) + } + + @Test + func closeOpenContainers_releasesSignedAndCryptoContainerReferences() async { + viewModel.closeOpenContainers() + + #expect(mockSharedContainerViewModel.setSignedContainerCallCount == 1) + #expect(mockSharedContainerViewModel.setSignedContainerArgValues.allSatisfy { $0 == nil }) + #expect(mockSharedContainerViewModel.setCryptoContainerCallCount == 1) + #expect(mockSharedContainerViewModel.setCryptoContainerArgValues.allSatisfy { $0 == nil }) + #expect(mockSharedContainerViewModel.clearContainersCallCount == 1) + } + + private func makeViewModel(sharedContainerViewModel: SharedContainerViewModelProtocol) -> HomeViewModel { + HomeViewModel( + sharedContainerViewModel: sharedContainerViewModel, + fileManager: mockFileManager, + fileUtil: mockFileUtil + ) + } + + private func makeSignedContainer() -> SignedContainer { + SignedContainer( + fileManager: mockFileManager, + containerUtil: mockContainerUtil + ) + } } diff --git a/RIADigiDocTests/ViewModel/Signing/RootView/SigningRootViewModelTests.swift b/RIADigiDocTests/ViewModel/Signing/RootView/SigningRootViewModelTests.swift new file mode 100644 index 00000000..8a814c56 --- /dev/null +++ b/RIADigiDocTests/ViewModel/Signing/RootView/SigningRootViewModelTests.swift @@ -0,0 +1,107 @@ +/* + * 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 Foundation +import Testing +import LibdigidocLibSwift +import CommonsLibMocks +import UtilsLibMocks + +@MainActor +struct SigningRootViewModelTests { + private let mockDataStore: DataStoreProtocolMock + private let mockFileManager: FileManagerProtocolMock + private let mockContainerUtil: ContainerUtilProtocolMock + private let sharedContainerViewModel: SharedContainerViewModel + private let viewModel: SigningRootViewModel + + init() async throws { + mockDataStore = DataStoreProtocolMock() + mockFileManager = FileManagerProtocolMock() + mockContainerUtil = ContainerUtilProtocolMock() + sharedContainerViewModel = SharedContainerViewModel() + viewModel = SigningRootViewModel( + dataStore: mockDataStore, + sharedContainerViewModel: sharedContainerViewModel + ) + } + + private func makeSignedContainer() -> SignedContainer { + SignedContainer( + fileManager: mockFileManager, + containerUtil: mockContainerUtil + ) + } + + @Test + func applySignedContainer_replacesContainerWhenItIsStillTheOpenOne() async { + let containerBeingSigned = makeSignedContainer() + sharedContainerViewModel.setSignedContainer(containerBeingSigned) + let signedContainer = makeSignedContainer() + + viewModel.applySignedContainer(signedContainer, replacing: containerBeingSigned) + + #expect(sharedContainerViewModel.currentContainer() === signedContainer) + #expect(sharedContainerViewModel.containers().count == 1) + #expect(sharedContainerViewModel.getIsSignatureAdded()) + } + + @Test + func applySignedContainer_dropsResultWhenContainersWereCleared() async { + let containerBeingSigned = makeSignedContainer() + sharedContainerViewModel.setSignedContainer(containerBeingSigned) + sharedContainerViewModel.clearContainers() + let signedContainer = makeSignedContainer() + + viewModel.applySignedContainer(signedContainer, replacing: containerBeingSigned) + + #expect(sharedContainerViewModel.containers().isEmpty) + #expect(!sharedContainerViewModel.getIsSignatureAdded()) + } + + @Test + func applySignedContainer_dropsResultWhenAnotherContainerWasOpened() async { + let containerBeingSigned = makeSignedContainer() + sharedContainerViewModel.setSignedContainer(containerBeingSigned) + sharedContainerViewModel.clearContainers() + + let externallyOpenedContainer = makeSignedContainer() + sharedContainerViewModel.setSignedContainer(externallyOpenedContainer) + + let signedContainer = makeSignedContainer() + + viewModel.applySignedContainer(signedContainer, replacing: containerBeingSigned) + + #expect(sharedContainerViewModel.currentContainer() === externallyOpenedContainer) + #expect(sharedContainerViewModel.containers().count == 1) + #expect(!sharedContainerViewModel.getIsSignatureAdded()) + } + + @Test + func applySignedContainer_dropsResultWhenThereWasNoContainerBeingSigned() async { + let externallyOpenedContainer = makeSignedContainer() + sharedContainerViewModel.setSignedContainer(externallyOpenedContainer) + let signedContainer = makeSignedContainer() + + viewModel.applySignedContainer(signedContainer, replacing: nil) + + #expect(sharedContainerViewModel.currentContainer() === externallyOpenedContainer) + #expect(!sharedContainerViewModel.getIsSignatureAdded()) + } +}