From 96445a214e69c985ceaf2f81addcc2ec0f83df20 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 30 Oct 2026 23:42:53 +0200 Subject: [PATCH] Block unsupported version when returning to the app --- RIADigiDoc.xcodeproj/project.pbxproj | 2 + RIADigiDoc/DI/AppContainer.swift | 8 ++- RIADigiDoc/RIADigiDocApp.swift | 52 ++++++++++++-- RIADigiDoc/Util/Version/AppVersionUtil.swift | 22 ++++++ .../Util/Version/AppVersionUtilProtocol.swift | 8 +++ RIADigiDoc/ViewModel/HomeViewModel.swift | 10 ++- .../Util/Version/AppVersionUtilTests.swift | 68 +++++++++++++++++++ .../ViewModel/HomeViewModelTests.swift | 19 +++++- .../TimeStampSettingsViewModelTests.swift | 2 + 9 files changed, 180 insertions(+), 11 deletions(-) create mode 100644 RIADigiDoc/Util/Version/AppVersionUtil.swift create mode 100644 RIADigiDoc/Util/Version/AppVersionUtilProtocol.swift create mode 100644 RIADigiDocTests/Util/Version/AppVersionUtilTests.swift diff --git a/RIADigiDoc.xcodeproj/project.pbxproj b/RIADigiDoc.xcodeproj/project.pbxproj index e012e7d9..f52308db 100644 --- a/RIADigiDoc.xcodeproj/project.pbxproj +++ b/RIADigiDoc.xcodeproj/project.pbxproj @@ -263,6 +263,8 @@ Util/Signature/SignatureUtilProtocol.swift, Util/Theme/ThemeSettings.swift, Util/Theme/ThemeSettingsProtocol.swift, + Util/Version/AppVersionUtil.swift, + Util/Version/AppVersionUtilProtocol.swift, Util/WebEid/WebEidUriUtil.swift, ViewModel/AdvancedSettingsViewModel.swift, ViewModel/CertificateDetailViewModel.swift, diff --git a/RIADigiDoc/DI/AppContainer.swift b/RIADigiDoc/DI/AppContainer.swift index 077cf8e1..9c25f347 100644 --- a/RIADigiDoc/DI/AppContainer.swift +++ b/RIADigiDoc/DI/AppContainer.swift @@ -81,7 +81,8 @@ extension Container { HomeViewModel( sharedContainerViewModel: self.sharedContainerViewModel(), fileManager: self.fileManager(), - fileUtil: self.fileUtil() + fileUtil: self.fileUtil(), + appVersionUtil: self.appVersionUtil() ) } } @@ -163,6 +164,11 @@ extension Container { self { CertificateUtil() } } + @MainActor + var appVersionUtil: Factory { + self { @MainActor in AppVersionUtil(configurationLoader: self.configurationLoader()) } + } + @MainActor public var proxyUtil: Factory { self { @MainActor in ProxyUtil( diff --git a/RIADigiDoc/RIADigiDocApp.swift b/RIADigiDoc/RIADigiDocApp.swift index af648942..84eb1d19 100644 --- a/RIADigiDoc/RIADigiDocApp.swift +++ b/RIADigiDoc/RIADigiDocApp.swift @@ -27,6 +27,9 @@ struct RIADigiDocApp: App, Loggable { private let fileUtil: FileUtilProtocol private let librarySetup: LibrarySetup private let documentsMigrator: DocumentsMigratorProtocol + private let appVersionUtil: AppVersionUtilProtocol + + @Environment(\.scenePhase) private var scenePhase init() { _languageSettings = State(wrappedValue: Container.shared.languageSettings()) @@ -40,6 +43,7 @@ struct RIADigiDocApp: App, Loggable { self.fileUtil = Container.shared.fileUtil() self.librarySetup = Container.shared.librarySetup() self.documentsMigrator = Container.shared.documentsMigrator() + self.appVersionUtil = Container.shared.appVersionUtil() } private func loadBlockingScreenLanguage() async { @@ -50,6 +54,41 @@ struct RIADigiDocApp: App, Loggable { } } + private func applyAppVersionSupport() async -> Bool { + guard await appVersionUtil.isAppVersionUnsupported() else { return false } + + await loadBlockingScreenLanguage() + await MainActor.run { + self.isAppVersionUnsupported = true + } + removePendingSharedFiles() + + return true + } + + private func removePendingSharedFiles() { + do { + try fileUtil.removeSharedFiles(url: Directories.getSharedFolder(fileManager: fileManager)) + } catch { + RIADigiDocApp.logger().error( + "Unable to remove pending shared files: \(String(reflecting: error), privacy: .public)" + ) + } + } + + private func onScenePhaseActive() { + guard isSetupComplete || isAppVersionUnsupported, !isJailbroken else { return } + + Task { + if isAppVersionUnsupported { + removePendingSharedFiles() + return + } + + _ = await applyAppVersionSupport() + } + } + private func onLaunchScreenViewAppear() { Task { if await JailbreakDetection.isDeviceJailbroken(fileManager: fileManager) { @@ -62,13 +101,7 @@ struct RIADigiDocApp: App, Loggable { await librarySetup.setupLibraries() - if await configurationLoader.getConfiguration()? - .isAppVersionUnsupported(BundleUtil.getAppVersion()) == true { - RIADigiDocApp.logger().error("App version \(BundleUtil.getAppVersion()) is unsupported") - await loadBlockingScreenLanguage() - await MainActor.run { - self.isAppVersionUnsupported = true - } + if await applyAppVersionSupport() { return } @@ -166,6 +199,11 @@ struct RIADigiDocApp: App, Loggable { } } .hideSensitiveContent() + .onChange(of: scenePhase) { _, newPhase in + if newPhase == .active { + onScenePhaseActive() + } + } } } } diff --git a/RIADigiDoc/Util/Version/AppVersionUtil.swift b/RIADigiDoc/Util/Version/AppVersionUtil.swift new file mode 100644 index 00000000..007ee3e8 --- /dev/null +++ b/RIADigiDoc/Util/Version/AppVersionUtil.swift @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +import ConfigLib +import UtilsLib + +public struct AppVersionUtil: AppVersionUtilProtocol, Loggable { + + let configurationLoader: ConfigurationLoaderProtocol + + public func isAppVersionUnsupported() async -> Bool { + let appVersion = BundleUtil.getAppVersion() + let isUnsupported = await configurationLoader.getConfiguration()? + .isAppVersionUnsupported(appVersion) == true + + if isUnsupported { + AppVersionUtil.logger().error("App version \(appVersion, privacy: .public) is unsupported") + } + + return isUnsupported + } +} diff --git a/RIADigiDoc/Util/Version/AppVersionUtilProtocol.swift b/RIADigiDoc/Util/Version/AppVersionUtilProtocol.swift new file mode 100644 index 00000000..cf9ad383 --- /dev/null +++ b/RIADigiDoc/Util/Version/AppVersionUtilProtocol.swift @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +/// @mockable +@MainActor +public protocol AppVersionUtilProtocol: Sendable { + func isAppVersionUnsupported() async -> Bool +} diff --git a/RIADigiDoc/ViewModel/HomeViewModel.swift b/RIADigiDoc/ViewModel/HomeViewModel.swift index 49cf00bc..dd0d2bda 100644 --- a/RIADigiDoc/ViewModel/HomeViewModel.swift +++ b/RIADigiDoc/ViewModel/HomeViewModel.swift @@ -19,15 +19,18 @@ class HomeViewModel: HomeViewModelProtocol, Loggable { private let sharedContainerViewModel: SharedContainerViewModelProtocol private let fileManager: FileManagerProtocol private let fileUtil: FileUtilProtocol + private let appVersionUtil: AppVersionUtilProtocol init( sharedContainerViewModel: SharedContainerViewModelProtocol, fileManager: FileManagerProtocol, - fileUtil: FileUtilProtocol + fileUtil: FileUtilProtocol, + appVersionUtil: AppVersionUtilProtocol ) { self.sharedContainerViewModel = sharedContainerViewModel self.fileManager = fileManager self.fileUtil = fileUtil + self.appVersionUtil = appVersionUtil } func didUserCancelFileOpening(isImportingValue: Bool, isFileOpeningLoading: Bool) -> Bool { @@ -72,6 +75,11 @@ class HomeViewModel: HomeViewModelProtocol, Loggable { } func getSharedFiles() async -> [URL] { + guard await !appVersionUtil.isAppVersionUnsupported() else { + HomeViewModel.logger().info("App version is unsupported. Ignoring shared files") + return [] + } + do { HomeViewModel.logger().info("Checking for shared files...") let sharedFolderURL = try await Directories.getSharedFolder(fileManager: fileManager) diff --git a/RIADigiDocTests/Util/Version/AppVersionUtilTests.swift b/RIADigiDocTests/Util/Version/AppVersionUtilTests.swift new file mode 100644 index 00000000..d372cf14 --- /dev/null +++ b/RIADigiDocTests/Util/Version/AppVersionUtilTests.swift @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +import ConfigLib +import ConfigLibMocks +import Foundation +import Testing + +@MainActor +final class AppVersionUtilTests { + private let appVersionUtil: AppVersionUtil + + private let mockConfigurationLoader: ConfigurationLoaderProtocolMock! + + init() { + mockConfigurationLoader = ConfigurationLoaderProtocolMock() + appVersionUtil = AppVersionUtil(configurationLoader: mockConfigurationLoader) + } + + private func configuration(unsupportedAppVersion: String?) throws -> ConfigurationProvider { + try TestConfigurationProvider.mockConfigurationProvider(unsupportedAppVersion: unsupportedAppVersion) + } + + @Test + func isAppVersionUnsupported_returnFalseWhenConfigurationIsMissing() async { + mockConfigurationLoader.getConfigurationHandler = { nil } + + #expect(await !appVersionUtil.isAppVersionUnsupported()) + } + + @Test + func isAppVersionUnsupported_returnFalseWhenUnsupportedVersionIsNotSet() async throws { + let provider = try configuration(unsupportedAppVersion: nil) + mockConfigurationLoader.getConfigurationHandler = { provider } + + #expect(await !appVersionUtil.isAppVersionUnsupported()) + } + + @Test + func isAppVersionUnsupported_returnTrueWhenAppVersionIsLower() async throws { + let provider = try configuration(unsupportedAppVersion: "999.0.0") + mockConfigurationLoader.getConfigurationHandler = { provider } + + #expect(await appVersionUtil.isAppVersionUnsupported()) + } + + @Test + func isAppVersionUnsupported_returnFalseWhenAppVersionIsHigher() async throws { + let provider = try configuration(unsupportedAppVersion: "0.0.1") + mockConfigurationLoader.getConfigurationHandler = { provider } + + #expect(await !appVersionUtil.isAppVersionUnsupported()) + } + + @Test + func isAppVersionUnsupported_reevaluatesConfigurationOnEveryCall() async throws { + let supported = try configuration(unsupportedAppVersion: nil) + mockConfigurationLoader.getConfigurationHandler = { supported } + + #expect(await !appVersionUtil.isAppVersionUnsupported()) + + let unsupported = try configuration(unsupportedAppVersion: "999.0.0") + mockConfigurationLoader.getConfigurationHandler = { unsupported } + + #expect(await appVersionUtil.isAppVersionUnsupported()) + #expect(mockConfigurationLoader.getConfigurationCallCount == 2) + } +} diff --git a/RIADigiDocTests/ViewModel/HomeViewModelTests.swift b/RIADigiDocTests/ViewModel/HomeViewModelTests.swift index 1fc6d0b1..beec959b 100644 --- a/RIADigiDocTests/ViewModel/HomeViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/HomeViewModelTests.swift @@ -15,6 +15,7 @@ struct HomeViewModelTests { private let mockFileManager: FileManagerProtocolMock private let mockFileUtil: FileUtilProtocolMock private let mockContainerUtil: ContainerUtilProtocolMock + private let mockAppVersionUtil: AppVersionUtilProtocolMock private let viewModel: HomeViewModel init() async throws { @@ -22,10 +23,13 @@ struct HomeViewModelTests { mockFileManager = FileManagerProtocolMock() mockFileUtil = FileUtilProtocolMock() mockContainerUtil = ContainerUtilProtocolMock() + mockAppVersionUtil = AppVersionUtilProtocolMock() + mockAppVersionUtil.isAppVersionUnsupportedHandler = { false } viewModel = HomeViewModel( sharedContainerViewModel: mockSharedContainerViewModel, fileManager: mockFileManager, - fileUtil: mockFileUtil + fileUtil: mockFileUtil, + appVersionUtil: mockAppVersionUtil ) } @@ -234,7 +238,8 @@ struct HomeViewModelTests { HomeViewModel( sharedContainerViewModel: sharedContainerViewModel, fileManager: mockFileManager, - fileUtil: mockFileUtil + fileUtil: mockFileUtil, + appVersionUtil: mockAppVersionUtil ) } @@ -244,4 +249,14 @@ struct HomeViewModelTests { containerUtil: mockContainerUtil ) } + + @Test + func getSharedFiles_returnsNothingWhenAppVersionIsUnsupported() async throws { + mockAppVersionUtil.isAppVersionUnsupportedHandler = { true } + + let shared = await viewModel.getSharedFiles() + + #expect(shared.isEmpty) + #expect(mockFileManager.contentsOfDirectoryCallCount == 0) + } } diff --git a/RIADigiDocTests/ViewModel/TimeStampSettingsViewModelTests.swift b/RIADigiDocTests/ViewModel/TimeStampSettingsViewModelTests.swift index 19023e9d..03dff1b9 100644 --- a/RIADigiDocTests/ViewModel/TimeStampSettingsViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/TimeStampSettingsViewModelTests.swift @@ -103,6 +103,8 @@ final class TimeStampSettingsViewModelTests { @Test func saveSettings_successWithManualSettingWithValidURL() async throws { + await viewModel.initializeSettings() + viewModel.selectedOption = .manualSetting let testURL = "http://valid.url.ee" viewModel.tsaUrl = testURL