[PM-26060] Create LanguageStateService in BWK (#2148)

This commit is contained in:
Katherine Bertelsen 2025-11-18 08:03:06 -06:00 committed by GitHub
parent 26c158b2e5
commit b5e7033d54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 50 additions and 16 deletions

View File

@ -62,6 +62,9 @@ public class ServiceContainer: Services {
/// The service used to import items.
let importItemsService: ImportItemsService
/// The state service that handles language state.
public let languageStateService: LanguageStateService
/// The service used to perform app data migrations.
let migrationService: MigrationService
@ -105,6 +108,7 @@ public class ServiceContainer: Services {
/// - exportItemsService: The service to export items.
/// - flightRecorder: The service used by the application for recording temporary debug logs.
/// - importItemsService: The service to import items.
/// - languageStateService: The service for handling language state.
/// - migrationService: The service to do data migrations
/// - notificationCenterService: The service used to receive foreground and background notifications.
/// - pasteboardService: The service used by the application for sharing data with other apps.
@ -129,6 +133,7 @@ public class ServiceContainer: Services {
exportItemsService: ExportItemsService,
flightRecorder: FlightRecorder,
importItemsService: ImportItemsService,
languageStateService: LanguageStateService,
migrationService: MigrationService,
notificationCenterService: NotificationCenterService,
pasteboardService: PasteboardService,
@ -152,6 +157,7 @@ public class ServiceContainer: Services {
self.exportItemsService = exportItemsService
self.flightRecorder = flightRecorder
self.importItemsService = importItemsService
self.languageStateService = languageStateService
self.migrationService = migrationService
self.notificationCenterService = notificationCenterService
self.pasteboardService = pasteboardService
@ -336,6 +342,7 @@ public class ServiceContainer: Services {
exportItemsService: exportItemsService,
flightRecorder: flightRecorder,
importItemsService: importItemsService,
languageStateService: stateService,
migrationService: migrationService,
notificationCenterService: notificationCenterService,
pasteboardService: pasteboardService,

View File

@ -15,6 +15,7 @@ typealias Services = HasAppInfoService
& HasExportItemsService
& HasFlightRecorder
& HasImportItemsService
& HasLanguageStateService
& HasNotificationCenterService
& HasPasteboardService
& HasStateService

View File

@ -154,7 +154,7 @@ enum StateServiceError: Error {
/// A default implementation of `StateService`.
///
actor DefaultStateService: StateService, ActiveAccountStateProvider, ConfigStateService, FlightRecorderStateService {
actor DefaultStateService: StateService, ActiveAccountStateProvider, ConfigStateService, FlightRecorderStateService, LanguageStateService {
// MARK: Properties
/// The language option currently selected for the app.

View File

@ -22,6 +22,7 @@ extension ServiceContainer {
exportItemsService: ExportItemsService = MockExportItemsService(),
flightRecorder: FlightRecorder = MockFlightRecorder(),
importItemsService: ImportItemsService = MockImportItemsService(),
languageStateService: LanguageStateService = MockLanguageStateService(),
migrationService: MigrationService = MockMigrationService(),
notificationCenterService: NotificationCenterService = MockNotificationCenterService(),
pasteboardService: PasteboardService = MockPasteboardService(),
@ -46,6 +47,7 @@ extension ServiceContainer {
exportItemsService: exportItemsService,
flightRecorder: flightRecorder,
importItemsService: importItemsService,
languageStateService: languageStateService,
migrationService: migrationService,
notificationCenterService: notificationCenterService,
pasteboardService: pasteboardService,

View File

@ -15,7 +15,7 @@ protocol SelectLanguageDelegate: AnyObject {
final class SelectLanguageProcessor: StateProcessor<SelectLanguageState, SelectLanguageAction, Void> {
// MARK: Types
typealias Services = HasStateService
typealias Services = HasLanguageStateService
// MARK: Properties
@ -71,7 +71,7 @@ final class SelectLanguageProcessor: StateProcessor<SelectLanguageState, SelectL
// Save the value.
state.currentLanguage = languageOption
services.stateService.appLanguage = languageOption
services.languageStateService.appLanguage = languageOption
delegate?.languageSelected(languageOption)
// Show the confirmation alert and close the view after the user clicks ok.

View File

@ -11,7 +11,7 @@ class SelectLanguageProcessorTests: BitwardenTestCase {
var coordinator: MockCoordinator<SettingsRoute, SettingsEvent>!
var delegate: MockSelectLanguageDelegate!
var stateService: MockStateService!
var languageStateService: MockLanguageStateService!
var subject: SelectLanguageProcessor!
// MARK: Setup & Teardown
@ -21,9 +21,9 @@ class SelectLanguageProcessorTests: BitwardenTestCase {
coordinator = MockCoordinator()
delegate = MockSelectLanguageDelegate()
stateService = MockStateService()
languageStateService = MockLanguageStateService()
let services = ServiceContainer.withMocks(
stateService: stateService,
languageStateService: languageStateService,
)
subject = SelectLanguageProcessor(
@ -39,7 +39,7 @@ class SelectLanguageProcessorTests: BitwardenTestCase {
coordinator = nil
delegate = nil
stateService = nil
languageStateService = nil
subject = nil
}
@ -59,7 +59,7 @@ class SelectLanguageProcessorTests: BitwardenTestCase {
subject.receive(.languageTapped(.custom(languageCode: "th")))
XCTAssertEqual(subject.state.currentLanguage, .custom(languageCode: "th"))
XCTAssertEqual(stateService.appLanguage, .custom(languageCode: "th"))
XCTAssertEqual(languageStateService.appLanguage, .custom(languageCode: "th"))
XCTAssertEqual(delegate.selectedLanguage, .custom(languageCode: "th"))
XCTAssertEqual(coordinator.alertShown.last, .languageChanged(to: LanguageOption("th").title) {})

View File

@ -26,6 +26,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
& HasExportItemsService
& HasFlightRecorder
& HasImportItemsService
& HasLanguageStateService
& HasPasteboardService
& HasStateService
& HasTimeProvider

View File

@ -0,0 +1,5 @@
/// A protocol for a State Service that handles language state.
public protocol LanguageStateService: AnyObject { // sourcery: AutoMockable
/// The language option currently selected for the app.
var appLanguage: LanguageOption { get set }
}

View File

@ -35,6 +35,13 @@ public protocol HasFlightRecorder {
var flightRecorder: FlightRecorder { get }
}
/// Protocol for an object that provides a `LanguageStateService`.
///
public protocol HasLanguageStateService {
/// The service used by the application to manage language state.
var languageStateService: LanguageStateService { get }
}
/// Protocol for an object that provides a `TimeProvider`.
///
public protocol HasTimeProvider {

View File

@ -112,6 +112,9 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
/// The repository used to manage keychain items.
let keychainRepository: KeychainRepository
/// The state service that handles language state.
public let languageStateService: LanguageStateService
/// The service used by the application to evaluate local auth policies.
let localAuthService: LocalAuthService
@ -233,6 +236,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
/// in Credential Exhange flow.
/// - keychainRepository: The repository used to manages keychain items.
/// - keychainService: The service used to access & store data on the device keychain.
/// - languageStateService: The service for handling language state.
/// - localAuthService: The service used by the application to evaluate local auth policies.
/// - migrationService: The serviced used to perform app data migrations.
/// - nfcReaderService: The service used by the application to read NFC tags.
@ -291,6 +295,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
importCiphersRepository: ImportCiphersRepository,
keychainRepository: KeychainRepository,
keychainService: KeychainService,
languageStateService: LanguageStateService,
localAuthService: LocalAuthService,
migrationService: MigrationService,
nfcReaderService: NFCReaderService,
@ -348,6 +353,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
self.importCiphersRepository = importCiphersRepository
self.keychainService = keychainService
self.keychainRepository = keychainRepository
self.languageStateService = languageStateService
self.localAuthService = localAuthService
self.migrationService = migrationService
self.nfcReaderService = nfcReaderService
@ -936,6 +942,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
importCiphersRepository: importCiphersRepository,
keychainRepository: keychainRepository,
keychainService: keychainService,
languageStateService: stateService,
localAuthService: localAuthService,
migrationService: migrationService,
nfcReaderService: nfcReaderService ?? NoopNFCReaderService(),

View File

@ -32,6 +32,7 @@ typealias Services = HasAPIService
& HasFlightRecorder
& HasGeneratorRepository
& HasImportCiphersRepository
& HasLanguageStateService
& HasLocalAuthService
& HasNFCReaderService
& HasNotificationCenterService

View File

@ -1437,7 +1437,7 @@ enum StateServiceError: LocalizedError {
/// A default implementation of `StateService`.
///
actor DefaultStateService: StateService, ActiveAccountStateProvider, ConfigStateService, FlightRecorderStateService { // swiftlint:disable:this type_body_length line_length
actor DefaultStateService: StateService, ActiveAccountStateProvider, ConfigStateService, FlightRecorderStateService, LanguageStateService { // swiftlint:disable:this type_body_length line_length
// MARK: Properties
/// The language option currently selected for the app.

View File

@ -39,6 +39,7 @@ extension ServiceContainer {
httpClient: HTTPClient = MockHTTPClient(),
keychainRepository: KeychainRepository = MockKeychainRepository(),
keychainService: KeychainService = MockKeychainService(),
languageStateService: LanguageStateService = MockLanguageStateService(),
localAuthService: LocalAuthService = MockLocalAuthService(),
migrationService: MigrationService = MockMigrationService(),
nfcReaderService: NFCReaderService = MockNFCReaderService(),
@ -100,6 +101,7 @@ extension ServiceContainer {
importCiphersRepository: importCiphersRepository,
keychainRepository: keychainRepository,
keychainService: keychainService,
languageStateService: languageStateService,
localAuthService: localAuthService,
migrationService: migrationService,
nfcReaderService: nfcReaderService,

View File

@ -16,7 +16,7 @@ protocol SelectLanguageDelegate: AnyObject {
final class SelectLanguageProcessor: StateProcessor<SelectLanguageState, SelectLanguageAction, Void> {
// MARK: Types
typealias Services = HasStateService
typealias Services = HasLanguageStateService
// MARK: Properties
@ -72,7 +72,7 @@ final class SelectLanguageProcessor: StateProcessor<SelectLanguageState, SelectL
// Save the value.
state.currentLanguage = languageOption
services.stateService.appLanguage = languageOption
services.languageStateService.appLanguage = languageOption
delegate?.languageSelected(languageOption)
// Show the confirmation alert and close the view after the user clicks ok.

View File

@ -11,7 +11,7 @@ class SelectLanguageProcessorTests: BitwardenTestCase {
var coordinator: MockCoordinator<SettingsRoute, SettingsEvent>!
var delegate: MockSelectLanguageDelegate!
var stateService: MockStateService!
var languageStateService: MockLanguageStateService!
var subject: SelectLanguageProcessor!
// MARK: Setup & Teardown
@ -21,9 +21,9 @@ class SelectLanguageProcessorTests: BitwardenTestCase {
coordinator = MockCoordinator()
delegate = MockSelectLanguageDelegate()
stateService = MockStateService()
languageStateService = MockLanguageStateService()
let services = ServiceContainer.withMocks(
stateService: stateService,
languageStateService: languageStateService,
)
subject = SelectLanguageProcessor(
@ -39,7 +39,7 @@ class SelectLanguageProcessorTests: BitwardenTestCase {
coordinator = nil
delegate = nil
stateService = nil
languageStateService = nil
subject = nil
}
@ -59,7 +59,7 @@ class SelectLanguageProcessorTests: BitwardenTestCase {
subject.receive(.languageTapped(.custom(languageCode: "th")))
XCTAssertEqual(subject.state.currentLanguage, .custom(languageCode: "th"))
XCTAssertEqual(stateService.appLanguage, .custom(languageCode: "th"))
XCTAssertEqual(languageStateService.appLanguage, .custom(languageCode: "th"))
XCTAssertEqual(delegate.selectedLanguage, .custom(languageCode: "th"))
XCTAssertEqual(coordinator.alertShown.last, .languageChanged(to: LanguageOption("th").title) {})

View File

@ -75,6 +75,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d
& HasExportCXFCiphersRepository
& HasExportVaultService
& HasFlightRecorder
& HasLanguageStateService
& HasNotificationCenterService
& HasPasteboardService
& HasPolicyService