From cae229cd11f14a5c97d7be7dba58c3df30345003 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 9 Oct 2026 16:13:03 +0300 Subject: [PATCH 1/2] Block app when version is marked unsupported --- .../Provider/ConfigurationProvider.swift | 20 ++++- .../Util/ConfigurationUtil.swift | 23 +++++ .../Provider/ConfigurationProviderTests.swift | 87 +++++++++++++++++++ .../Util/ConfigurationUtilTests.swift | 38 ++++++++ .../Mocks/TestConfigurationProvider.swift | 6 +- RIADigiDoc/RIADigiDocApp.swift | 18 ++++ .../Supporting files/Localizable.xcstrings | 18 ++++ .../UI/Component/UnsupportedVersionView.swift | 63 ++++++++++++++ 8 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Provider/ConfigurationProviderTests.swift create mode 100644 RIADigiDoc/UI/Component/UnsupportedVersionView.swift diff --git a/Modules/ConfigLib/Sources/ConfigLib/Configuration/Provider/ConfigurationProvider.swift b/Modules/ConfigLib/Sources/ConfigLib/Configuration/Provider/ConfigurationProvider.swift index e02d5019..918af014 100644 --- a/Modules/ConfigLib/Sources/ConfigLib/Configuration/Provider/ConfigurationProvider.swift +++ b/Modules/ConfigLib/Sources/ConfigLib/Configuration/Provider/ConfigurationProvider.swift @@ -21,7 +21,7 @@ extension Dictionary where Key == String, Value == ConfigurationProvider.CDOC2Co } } -public struct ConfigurationProvider: Codable, Sendable, Equatable { +public struct ConfigurationProvider: Codable, Sendable, Equatable, Loggable { public struct MetaInf: Codable, Sendable, Equatable { public let url: String public let date: String @@ -95,6 +95,7 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { public let cdoc2DefaultKeyserver: String public let cdoc2UseKeyserver: Bool public let cdoc2Conf: [String: CDOC2Conf] + public let unsupportedAppVersion: String? private enum CodingKeys: String, CodingKey { case metaInf = "META-INF" @@ -117,6 +118,7 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { case cdoc2DefaultKeyserver = "CDOC2-DEFAULT-KEYSERVER" case cdoc2UseKeyserver = "CDOC2-USE-KEYSERVER" case cdoc2Conf = "CDOC2-CONF" + case unsupportedAppVersion = "RIADD-UNSUPPORTED" } public func encode(to encoder: Encoder) throws { @@ -147,6 +149,7 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { try container.encode(cdoc2DefaultKeyserver, forKey: .cdoc2DefaultKeyserver) try container.encode(cdoc2UseKeyserver, forKey: .cdoc2UseKeyserver) try container.encode(cdoc2Conf, forKey: .cdoc2Conf) + try container.encodeIfPresent(unsupportedAppVersion, forKey: .unsupportedAppVersion) } public init(from decoder: Decoder) throws { @@ -170,6 +173,14 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { cdoc2DefaultKeyserver = try container.decode(String.self, forKey: .cdoc2DefaultKeyserver) cdoc2UseKeyserver = try container.decode(Bool.self, forKey: .cdoc2UseKeyserver) cdoc2Conf = try container.decode([String: CDOC2Conf].self, forKey: .cdoc2Conf) + do { + unsupportedAppVersion = try container.decodeIfPresent(String.self, forKey: .unsupportedAppVersion) + } catch { + ConfigurationProvider.logger().error( + "Unable to decode unsupported app version: \(String(reflecting: error), privacy: .public)" + ) + unsupportedAppVersion = nil + } let lastUpdateCheckString = try container.decodeIfPresent( String.self, @@ -202,6 +213,7 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { cdoc2DefaultKeyserver: String, cdoc2UseKeyserver: Bool, cdoc2Conf: [String: CDOC2Conf], + unsupportedAppVersion: String? = nil ) { self.metaInf = metaInf self.sivaUrl = sivaUrl @@ -223,5 +235,11 @@ public struct ConfigurationProvider: Codable, Sendable, Equatable { self.cdoc2DefaultKeyserver = cdoc2DefaultKeyserver self.cdoc2UseKeyserver = cdoc2UseKeyserver self.cdoc2Conf = cdoc2Conf + self.unsupportedAppVersion = unsupportedAppVersion + } + + public func isAppVersionUnsupported(_ appVersion: String) -> Bool { + guard let unsupportedAppVersion else { return false } + return ConfigurationUtil.isVersion(appVersion, lowerThan: unsupportedAppVersion) } } diff --git a/Modules/ConfigLib/Sources/ConfigLib/Configuration/Util/ConfigurationUtil.swift b/Modules/ConfigLib/Sources/ConfigLib/Configuration/Util/ConfigurationUtil.swift index be3d7a5a..1a0e3316 100644 --- a/Modules/ConfigLib/Sources/ConfigLib/Configuration/Util/ConfigurationUtil.swift +++ b/Modules/ConfigLib/Sources/ConfigLib/Configuration/Util/ConfigurationUtil.swift @@ -19,4 +19,27 @@ struct ConfigurationUtil { return encoded == decodedData.base64EncodedString() } + + static func isVersion(_ version: String, lowerThan otherVersion: String) -> Bool { + guard let components = versionComponents(version), + let otherComponents = versionComponents(otherVersion) else { + return false + } + + let count = max(components.count, otherComponents.count) + let padded = components + Array(repeating: 0, count: count - components.count) + let otherPadded = otherComponents + Array(repeating: 0, count: count - otherComponents.count) + + return padded.lexicographicallyPrecedes(otherPadded) + } + + private static func versionComponents(_ version: String) -> [Int]? { + let parts = version + .trimmingCharacters(in: .whitespaces) + .split(separator: ".", omittingEmptySubsequences: false) + let components = parts.compactMap { part in + part.allSatisfy({ $0.isASCII && $0.isNumber }) ? Int(part) : nil + } + return components.count == parts.count ? components : nil + } } diff --git a/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Provider/ConfigurationProviderTests.swift b/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Provider/ConfigurationProviderTests.swift new file mode 100644 index 00000000..1016f77f --- /dev/null +++ b/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Provider/ConfigurationProviderTests.swift @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +import ConfigLibMocks +import Foundation +import Testing + +@testable import ConfigLib + +struct ConfigurationProviderTests { + + @Test + func isAppVersionUnsupported_returnFalseWhenUnsupportedVersionIsMissing() throws { + let provider = try TestConfigurationProvider.mockConfigurationProvider() + + #expect(!provider.isAppVersionUnsupported("0.0.1")) + } + + @Test + func isAppVersionUnsupported_returnTrueWhenAppVersionIsLower() throws { + let provider = try TestConfigurationProvider.mockConfigurationProvider(unsupportedAppVersion: "3.2.1") + + #expect(provider.isAppVersionUnsupported("3.2.0.100")) + } + + @Test + func isAppVersionUnsupported_returnFalseWhenAppVersionIsEqualOrHigher() throws { + let provider = try TestConfigurationProvider.mockConfigurationProvider(unsupportedAppVersion: "3.2.1") + + #expect(!provider.isAppVersionUnsupported("3.2.1.0")) + #expect(!provider.isAppVersionUnsupported("3.3.0.1")) + } + + @Test + func decode_readsUnsupportedAppVersion() throws { + let provider = try TestConfigurationProvider.mockConfigurationProvider(unsupportedAppVersion: "3.2.1") + + let decoded = try JSONDecoder().decode(ConfigurationProvider.self, from: JSONEncoder().encode(provider)) + + #expect(decoded.unsupportedAppVersion == "3.2.1") + } + + @Test + func decode_returnNilUnsupportedAppVersionWhenKeyIsMissing() throws { + let provider = try TestConfigurationProvider.mockConfigurationProvider() + + let data = try JSONEncoder().encode(provider) + let json = try #require(String(data: data, encoding: .utf8)) + let decoded = try JSONDecoder().decode(ConfigurationProvider.self, from: data) + + #expect(!json.contains("RIADD-UNSUPPORTED")) + #expect(decoded.unsupportedAppVersion == nil) + } + + @Test(arguments: ["", "3.2.1-beta", "abc"]) + func isAppVersionUnsupported_returnFalseWhenUnsupportedVersionIsInvalid(unsupportedAppVersion: String) throws { + let provider = try TestConfigurationProvider.mockConfigurationProvider( + unsupportedAppVersion: unsupportedAppVersion + ) + + #expect(!provider.isAppVersionUnsupported("3.2.0.1")) + } + + @Test + func isAppVersionUnsupported_returnFalseWhenAppVersionIsUnknown() throws { + let provider = try TestConfigurationProvider.mockConfigurationProvider(unsupportedAppVersion: "99.0.0") + + #expect(!provider.isAppVersionUnsupported("-.-")) + } + + @Test + func decode_ignoresNonStringUnsupportedAppVersion() throws { + let provider = try TestConfigurationProvider.mockConfigurationProvider() + var json = try #require( + JSONSerialization.jsonObject(with: JSONEncoder().encode(provider)) as? [String: Any] + ) + json["RIADD-UNSUPPORTED"] = 3.3 + + let decoded = try JSONDecoder().decode( + ConfigurationProvider.self, + from: JSONSerialization.data(withJSONObject: json) + ) + + #expect(decoded.unsupportedAppVersion == nil) + #expect(decoded.sivaUrl == provider.sivaUrl) + } +} diff --git a/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Util/ConfigurationUtilTests.swift b/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Util/ConfigurationUtilTests.swift index f24ac09c..1ed0c65b 100644 --- a/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Util/ConfigurationUtilTests.swift +++ b/Modules/ConfigLib/Tests/ConfigLibTests/Configuration/Util/ConfigurationUtilTests.swift @@ -64,4 +64,42 @@ final class ConfigurationUtilTests { let base64WithWhitespace = " SGVsbG8gd29ybGQ= " #expect(!ConfigurationUtil.isBase64(encoded: base64WithWhitespace)) } + + @Test(arguments: [ + ("3.2.0", "3.2.1"), + ("3.2.0.100", "3.2.1"), + ("3.2.0.100", "3.2.0.101"), + ("3.2", "3.2.0.1"), + ("3.9.0", "3.10.0"), + ("2.99.99", "3") + ]) + func isVersionLowerThan_returnTrueWhenVersionIsLower(version: String, otherVersion: String) { + #expect(ConfigurationUtil.isVersion(version, lowerThan: otherVersion)) + } + + @Test(arguments: [ + ("3.2.1", "3.2.1"), + ("3.2.1.0", "3.2.1"), + ("3.2.1", "3.2.1.0"), + ("3.2.1.5", "3.2.1"), + ("3.10.0", "3.9.0"), + ("4", "3.99.99") + ]) + func isVersionLowerThan_returnFalseWhenVersionIsEqualOrHigher(version: String, otherVersion: String) { + #expect(!ConfigurationUtil.isVersion(version, lowerThan: otherVersion)) + } + + @Test(arguments: [ + ("3.2.0", ""), + ("3.2.0", "abc"), + ("3.2.0", "3.2.x"), + ("3.2.0", "3..2"), + ("3.2.0", "+4.0"), + ("3.2.0", "-4.0"), + ("-.-", "4.0.0"), + ("", "4.0.0") + ]) + func isVersionLowerThan_returnFalseWhenEitherVersionIsInvalid(version: String, otherVersion: String) { + #expect(!ConfigurationUtil.isVersion(version, lowerThan: otherVersion)) + } } diff --git a/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift b/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift index 019b86ed..5f9a7c3e 100644 --- a/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift +++ b/Modules/ConfigLib/Tests/Mocks/TestConfigurationProvider.swift @@ -43,7 +43,8 @@ public class TestConfigurationProvider { cdoc2ConfUUID: String = "00000000-0000-0000-0000-000000000000", cdoc2ConfName: String = "RIA", cdoc2ConfPostUrl: String = "https://cdoc2.example.ee:8443", - cdoc2ConfFetchUrl: String = "https://cdoc2.example.ee:8444" + cdoc2ConfFetchUrl: String = "https://cdoc2.example.ee:8444", + unsupportedAppVersion: String? = nil ) throws -> ConfigurationProvider { let metaInf = ConfigurationProvider.MetaInf( url: metaInfUrl, @@ -98,7 +99,8 @@ public class TestConfigurationProvider { cdoc2Default: cdoc2Default, cdoc2DefaultKeyserver: cdoc2DefaultKeyserver, cdoc2UseKeyserver: cdoc2UseKeyserver, - cdoc2Conf: cdoc2Conf + cdoc2Conf: cdoc2Conf, + unsupportedAppVersion: unsupportedAppVersion ) } diff --git a/RIADigiDoc/RIADigiDocApp.swift b/RIADigiDoc/RIADigiDocApp.swift index d5624947..aaf6d529 100644 --- a/RIADigiDoc/RIADigiDocApp.swift +++ b/RIADigiDoc/RIADigiDocApp.swift @@ -15,6 +15,7 @@ struct RIADigiDocApp: App, Loggable { @State private var isSetupComplete = false @State private var isJailbroken: Bool = false + @State private var isAppVersionUnsupported: Bool = false @State private var isInitialLanguageSelected: Bool = false @State private var pathManager = NavigationPathManager() @@ -51,6 +52,17 @@ 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 languageSettings.loadSelectedLanguage() + await MainActor.run { + self.isAppVersionUnsupported = true + } + return + } + await crashReportManager.evaluateCrashReporting() fileUtil.removeSavedFilesDirectory(savedFilesDirectory: nil) @@ -84,6 +96,12 @@ struct RIADigiDocApp: App, Loggable { .environment(\.typography, Typography.current()) .environment(themeSettings) .preferredColorScheme(currentTheme.colorScheme) + } else if isAppVersionUnsupported { + UnsupportedVersionView() + .environment(\.typography, Typography.current()) + .environment(languageSettings) + .environment(themeSettings) + .preferredColorScheme(currentTheme.colorScheme) } else if isSetupComplete { NavigationStack(path: $pathManager.path) { if isInitialLanguageSelected { diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index e5e4fc73..c62ba934 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -127,6 +127,24 @@ } } }, + "App version unsupported message" : { + "comment" : "Full-screen message when central configuration marks this app version unsupported", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This version of ID-software on your computer is unsupported. RIA DigiDoc cannot be used until you update ID-software. Install new ID-software from [www.id.ee](https://www.id.ee/en/article/install-id-software/)." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sinu nutiseadmes olev ID-tarkvara ei ole toetatud. RIA Digidoc kasutamiseks pead ID-tarkvara uuendama. Paigalda uus ID-tarkvara veebilehelt [www.id.ee](https://www.id.ee/artikkel/paigalda-id-tarkvara/)." + } + } + } + }, "Archive timestamp" : { "comment" : "Archive timestamp label in signature details", "extractionState" : "manual", diff --git a/RIADigiDoc/UI/Component/UnsupportedVersionView.swift b/RIADigiDoc/UI/Component/UnsupportedVersionView.swift new file mode 100644 index 00000000..0361c8dd --- /dev/null +++ b/RIADigiDoc/UI/Component/UnsupportedVersionView.swift @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +import SwiftUI +import FactoryKit +import UtilsLib + +struct UnsupportedVersionView: View, Loggable { + @AppTheme private var theme + @AppTypography private var typography + @Environment(LanguageSettings.self) private var languageSettings + + private var message: AttributedString { + let text = languageSettings.localized("App version unsupported message") + do { + return try AttributedString(markdown: text) + } catch { + UnsupportedVersionView.logger().error( + "Unable to parse unsupported version message: \(String(reflecting: error), privacy: .public)" + ) + return AttributedString(text) + } + } + + var body: some View { + ZStack { + theme.surface.ignoresSafeArea() + + ScrollView { + if let url = message.runs.compactMap(\.link).first { + Link(destination: url) { + messageText + } + } else { + messageText + } + } + .defaultScrollAnchor(.center, for: .alignment) + } + } + + private var messageText: some View { + var styledMessage = message + for run in styledMessage.runs where run.link != nil { + styledMessage[run.range].foregroundColor = theme.primary + styledMessage[run.range].underlineStyle = .single + styledMessage[run.range].link = nil + } + + return Text(styledMessage) + .foregroundStyle(theme.onSurface) + .font(typography.bodyLarge) + .multilineTextAlignment(.center) + .padding() + .frame(maxWidth: .infinity) + } +} + +#Preview { + UnsupportedVersionView() + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) +} From a4e8b08ca97fba1b122b760124454b9183102428 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Tue, 20 Oct 2026 21:11:28 +0300 Subject: [PATCH 2/2] Show blocking screens in the user's language --- RIADigiDoc/RIADigiDocApp.swift | 12 +++++- .../Supporting files/Localizable.xcstrings | 4 +- .../Util/Language/LanguageSettings.swift | 8 ++++ .../Util/Language/LanguageSettingsTests.swift | 42 ++++++++++++++++++- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/RIADigiDoc/RIADigiDocApp.swift b/RIADigiDoc/RIADigiDocApp.swift index aaf6d529..1b4a4ba0 100644 --- a/RIADigiDoc/RIADigiDocApp.swift +++ b/RIADigiDoc/RIADigiDocApp.swift @@ -42,9 +42,18 @@ struct RIADigiDocApp: App, Loggable { self.documentsMigrator = Container.shared.documentsMigrator() } + private func loadBlockingScreenLanguage() async { + if await dataStore.getIsInitialLanguageSelected() { + await languageSettings.loadSelectedLanguage() + } else { + languageSettings.loadDeviceLanguage() + } + } + private func onLaunchScreenViewAppear() { Task { if await JailbreakDetection.isDeviceJailbroken(fileManager: fileManager) { + await loadBlockingScreenLanguage() await MainActor.run { self.isJailbroken = true } @@ -56,7 +65,7 @@ struct RIADigiDocApp: App, Loggable { if await configurationLoader.getConfiguration()? .isAppVersionUnsupported(BundleUtil.getAppVersion()) == true { RIADigiDocApp.logger().error("App version \(BundleUtil.getAppVersion()) is unsupported") - await languageSettings.loadSelectedLanguage() + await loadBlockingScreenLanguage() await MainActor.run { self.isAppVersionUnsupported = true } @@ -94,6 +103,7 @@ struct RIADigiDocApp: App, Loggable { if isJailbroken { JailbreakView() .environment(\.typography, Typography.current()) + .environment(languageSettings) .environment(themeSettings) .preferredColorScheme(currentTheme.colorScheme) } else if isAppVersionUnsupported { diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index c62ba934..7c01c3ad 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -134,13 +134,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "This version of ID-software on your computer is unsupported. RIA DigiDoc cannot be used until you update ID-software. Install new ID-software from [www.id.ee](https://www.id.ee/en/article/install-id-software/)." + "value" : "This version of ID-software on your device is unsupported. RIA DigiDoc cannot be used until you update ID-software. Install new ID-software from [www.id.ee](https://www.id.ee/en/article/install-id-software/)" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Sinu nutiseadmes olev ID-tarkvara ei ole toetatud. RIA Digidoc kasutamiseks pead ID-tarkvara uuendama. Paigalda uus ID-tarkvara veebilehelt [www.id.ee](https://www.id.ee/artikkel/paigalda-id-tarkvara/)." + "value" : "Sinu nutiseadmes olev ID-tarkvara ei ole toetatud. RIA DigiDoc kasutamiseks pead mobiilirakendust uuendama. Paigalda uus ID-tarkvara veebilehelt [www.id.ee](https://www.id.ee/artikkel/paigalda-id-tarkvara/)" } } } diff --git a/RIADigiDoc/Util/Language/LanguageSettings.swift b/RIADigiDoc/Util/Language/LanguageSettings.swift index 15c95b4b..9c850750 100644 --- a/RIADigiDoc/Util/Language/LanguageSettings.swift +++ b/RIADigiDoc/Util/Language/LanguageSettings.swift @@ -29,6 +29,14 @@ public final class LanguageSettings: LanguageSettingsProtocol { localizedBundle = Bundle.main.path(forResource: selectedLanguage, ofType: "lproj").flatMap(Bundle.init) } + public func loadDeviceLanguage(preferredLocalizations: [String] = Bundle.main.preferredLocalizations) { + let supportedCodes = supportedLanguages.map(\.code) + let deviceLanguage = preferredLocalizations.first { supportedCodes.contains($0) } ?? DefaultValues.language + + selectedLanguage = deviceLanguage + localizedBundle = Bundle.main.path(forResource: deviceLanguage, ofType: "lproj").flatMap(Bundle.init) + } + public func getSelectedLanguage() -> String { return selectedLanguage } diff --git a/RIADigiDocTests/Util/Language/LanguageSettingsTests.swift b/RIADigiDocTests/Util/Language/LanguageSettingsTests.swift index 3d8800e8..f8fc8407 100644 --- a/RIADigiDocTests/Util/Language/LanguageSettingsTests.swift +++ b/RIADigiDocTests/Util/Language/LanguageSettingsTests.swift @@ -7,7 +7,7 @@ import Testing struct LanguageSettingsTests { private let mockDataStore: DataStoreProtocolMock - private let languageSettings: LanguageSettingsProtocol + private let languageSettings: LanguageSettings init() async { mockDataStore = DataStoreProtocolMock() @@ -63,4 +63,44 @@ struct LanguageSettingsTests { #expect(url.scheme == "https", "'\(key)' is not an https URL in \(languageCode)") } } + + @Test(arguments: [ + (["et", "en"], "et"), + (["en", "et"], "en"), + (["et"], "et") + ]) + func loadDeviceLanguage_usesFirstSupportedPreferredLocalization( + preferredLocalizations: [String], + expectedLanguage: String + ) async { + await languageSettings.loadDeviceLanguage(preferredLocalizations: preferredLocalizations) + + #expect(await languageSettings.getSelectedLanguage() == expectedLanguage) + } + + @Test(arguments: [ + ["Base", "et"], + ["lv", "et"] + ]) + func loadDeviceLanguage_skipsUnsupportedLocalizations(preferredLocalizations: [String]) async { + await languageSettings.loadDeviceLanguage(preferredLocalizations: preferredLocalizations) + + #expect(await languageSettings.getSelectedLanguage() == "et") + } + + @Test(arguments: [[], ["lv"], ["de", "fi"]]) + func loadDeviceLanguage_fallsBackToEnglishWhenNoSupportedLocalization( + preferredLocalizations: [String] + ) async { + await languageSettings.loadDeviceLanguage(preferredLocalizations: preferredLocalizations) + + #expect(await languageSettings.getSelectedLanguage() == "en") + } + + @Test + func loadDeviceLanguage_doesNotPersistSelection() async { + await languageSettings.loadDeviceLanguage(preferredLocalizations: ["et"]) + + #expect(mockDataStore.setSelectedLanguageCallCount == 0) + } }