From e3b557cf0cd0b8127a77edcd9942029a2dbfd3e6 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 15 Feb 2023 13:07:06 +0200 Subject: [PATCH] Fixes #322 - Developer options menu (#581) --- .../en.lproj/Untranslated.strings | 1 + .../Sources/Application/AppSettings.swift | 9 +++ .../Generated/Strings+Untranslated.swift | 2 + .../Other/EffectsScene/ConfettiScene.scn | Bin 0 -> 11782 bytes .../Other/EffectsScene/EffectsScene.swift | 74 ++++++++++++++++++ .../Other/EffectsScene/EffectsView.swift | 58 ++++++++++++++ .../DeveloperOptionsScreenCoordinator.swift | 29 +++++++ .../DeveloperOptionsScreenModels.swift | 27 +++++++ .../DeveloperOptionsScreenViewModel.swift | 27 +++++++ ...eloperOptionsScreenViewModelProtocol.swift | 23 ++++++ .../View/DeveloperOptionsScreenScreen.swift | 64 +++++++++++++++ .../Screens/RoomScreen/RoomScreenModels.swift | 2 +- .../RoomScreen/RoomScreenViewModel.swift | 2 +- .../Screens/RoomScreen/View/RoomScreen.swift | 2 +- ...reen.swift => TimelineItemDebugView.swift} | 2 +- .../Settings/SettingsScreenCoordinator.swift | 7 ++ .../Settings/SettingsScreenModels.swift | 3 + .../Settings/SettingsScreenViewModel.swift | 7 +- .../Settings/View/SettingsScreen.swift | 16 +++- .../DeveloperOptionsScreenScreenUITests.swift | 20 +++++ ...DeveloperOptionsScreenViewModelTests.swift | 22 ++++++ 21 files changed, 390 insertions(+), 7 deletions(-) create mode 100644 ElementX/Sources/Other/EffectsScene/ConfettiScene.scn create mode 100644 ElementX/Sources/Other/EffectsScene/EffectsScene.swift create mode 100644 ElementX/Sources/Other/EffectsScene/EffectsView.swift create mode 100644 ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenCoordinator.swift create mode 100644 ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift create mode 100644 ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift create mode 100644 ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/DeveloperOptionsScreen/View/DeveloperOptionsScreenScreen.swift rename ElementX/Sources/Screens/RoomScreen/View/{DebugScreen.swift => TimelineItemDebugView.swift} (97%) create mode 100644 UITests/Sources/DeveloperOptionsScreenScreenUITests.swift create mode 100644 UnitTests/Sources/DeveloperOptionsScreenViewModelTests.swift diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index ee73a44bb..3d80cdff3 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -14,6 +14,7 @@ "settings_appearance" = "Appearance"; "settings_timeline_style" = "Message layout"; "settings_session_verification" = "Complete verification"; +"settings_developer_options" = "Developer options"; "room_timeline_style_plain_long_description" = "Modern"; "room_timeline_style_bubbled_long_description" = "Bubbles"; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index e0559f357..63f212c00 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -49,6 +49,15 @@ final class AppSettings: ObservableObject { // MARK: - Application + lazy var canShowDeveloperOptions: Bool = { + #if DEBUG + true + #else + let apps = ["io.element.elementx.nightly", "io.element.elementx.pr"] + return apps.contains(InfoPlistReader.main.baseBundleIdentifier) + #endif + }() + /// The last known version of the app that was launched on this device, which is /// used to detect when migrations should be run. When `nil` the app may have been /// deleted between runs so should clear data in the shared container and keychain. diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index 52ee744e7..d39867a5f 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -140,6 +140,8 @@ extension ElementL10n { public static let sessionVerificationStart = ElementL10n.tr("Untranslated", "session_verification_start") /// Appearance public static let settingsAppearance = ElementL10n.tr("Untranslated", "settings_appearance") + /// Developer options + public static let settingsDeveloperOptions = ElementL10n.tr("Untranslated", "settings_developer_options") /// Complete verification public static let settingsSessionVerification = ElementL10n.tr("Untranslated", "settings_session_verification") /// Message layout diff --git a/ElementX/Sources/Other/EffectsScene/ConfettiScene.scn b/ElementX/Sources/Other/EffectsScene/ConfettiScene.scn new file mode 100644 index 0000000000000000000000000000000000000000..2d914d31ccb20c7f8cb5d8a7b3da0f9fd622273f GIT binary patch literal 11782 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 j EffectsScene? { + guard let scene = EffectsScene(named: Constants.confettiSceneName) else { return nil } + + let colors: [[Float]] = Color.element.contentAndAvatars.compactMap(\.floatComponents) + + if let particles = scene.rootNode.childNode(withName: Constants.particlesNodeName, recursively: false)?.particleSystems?.first { + // The particles need a non-zero color variation for the handler to affect the color + particles.particleColorVariation = SCNVector4(x: 0, y: 0, z: 0, w: 0.1) + + // Add a handler to customize the color of the particles. + particles.handle(.birth, forProperties: [.color]) { data, dataStride, _, count in + for index in 0.. SCNView { + SCNView(frame: .zero) + } + + func updateUIView(_ sceneView: SCNView, context: Context) { + sceneView.scene = makeScene() + sceneView.backgroundColor = .clear + } + + // MARK: - Private + + private func makeScene() -> EffectsScene? { + switch effect { + case .confetti: + return EffectsScene.confetti() + case .none: + return nil + } + } +} + +struct EffectsView_Previews: PreviewProvider { + static var previews: some View { + EffectsView(effect: .confetti) + } +} diff --git a/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenCoordinator.swift b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenCoordinator.swift new file mode 100644 index 000000000..99e8002a0 --- /dev/null +++ b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenCoordinator.swift @@ -0,0 +1,29 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +final class DeveloperOptionsScreenCoordinator: CoordinatorProtocol { + private let viewModel: DeveloperOptionsScreenViewModelProtocol + + init() { + viewModel = DeveloperOptionsScreenViewModel() + } + + func toPresentable() -> AnyView { + AnyView(DeveloperOptionsScreenScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift new file mode 100644 index 000000000..42fafe250 --- /dev/null +++ b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -0,0 +1,27 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum DeveloperOptionsScreenViewModelAction { } + +struct DeveloperOptionsScreenViewState: BindableState { + var bindings: DeveloperOptionsScreenViewStateBindings +} + +struct DeveloperOptionsScreenViewStateBindings { } + +enum DeveloperOptionsScreenViewAction { } diff --git a/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift new file mode 100644 index 000000000..72c8dbf71 --- /dev/null +++ b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -0,0 +1,27 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +typealias DeveloperOptionsScreenViewModelType = StateStoreViewModel + +class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, DeveloperOptionsScreenViewModelProtocol { + var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)? + + init() { + super.init(initialViewState: DeveloperOptionsScreenViewState(bindings: DeveloperOptionsScreenViewStateBindings())) + } +} diff --git a/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModelProtocol.swift new file mode 100644 index 000000000..257b5754d --- /dev/null +++ b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@MainActor +protocol DeveloperOptionsScreenViewModelProtocol { + var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)? { get set } + var context: DeveloperOptionsScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/DeveloperOptionsScreen/View/DeveloperOptionsScreenScreen.swift b/ElementX/Sources/Screens/DeveloperOptionsScreen/View/DeveloperOptionsScreenScreen.swift new file mode 100644 index 000000000..3ad177261 --- /dev/null +++ b/ElementX/Sources/Screens/DeveloperOptionsScreen/View/DeveloperOptionsScreenScreen.swift @@ -0,0 +1,64 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DeveloperOptionsScreenScreen: View { + @ObservedObject var context: DeveloperOptionsScreenViewModel.Context + @State private var showConfetti = false + + var body: some View { + Form { + Section { + Button { + showConfetti = true + } label: { + Text("🥳") + .frame(maxWidth: .infinity) + } + } + .listRowBackground(Color.element.formRowBackground) + } + .overlay(effectsView) + .scrollContentBackground(.hidden) + .background(Color.element.formBackground.ignoresSafeArea()) + .navigationTitle(ElementL10n.settingsDeveloperOptions) + .navigationBarTitleDisplayMode(.inline) + } + + @ViewBuilder + private var effectsView: some View { + if showConfetti { + EffectsView(effect: .confetti) + .task { await removeConfettiAfterDelay() } + } + } + + private func removeConfettiAfterDelay() async { + try? await Task.sleep(for: .seconds(4)) + showConfetti = false + } +} + +// MARK: - Previews + +struct DeveloperOptionsScreen_Previews: PreviewProvider { + static var previews: some View { + let viewModel = DeveloperOptionsScreenViewModel() + DeveloperOptionsScreenScreen(context: viewModel.context) + .tint(.element.accent) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 3f4e571b7..4fedfe12c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -88,7 +88,7 @@ struct RoomScreenViewStateBindings { /// Information describing the currently displayed alert. var alertInfo: AlertInfo? - var debugInfo: DebugScreen.DebugInfo? + var debugInfo: TimelineItemDebugView.DebugInfo? } enum RoomScreenErrorType: Hashable { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index b5101951b..74574ee8d 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -229,7 +229,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actions.append(.redact) } - var debugActions: [TimelineItemContextMenuAction] = [.viewSource] + var debugActions: [TimelineItemContextMenuAction] = ServiceLocator.shared.settings.canShowDeveloperOptions ? [.viewSource] : [] if let item = timelineItem as? EncryptedRoomTimelineItem, case let .megolmV1AesSha2(sessionID) = item.encryptionType { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 4094cbf07..38a6d1a0a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -30,7 +30,7 @@ struct RoomScreen: View { .toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background. .overlay { loadingIndicator } .alert(item: $context.alertInfo) { $0.alert } - .sheet(item: $context.debugInfo) { DebugScreen(info: $0) } + .sheet(item: $context.debugInfo) { TimelineItemDebugView(info: $0) } .task(id: context.viewState.roomId) { // Give a couple of seconds for items to load and to see them. try? await Task.sleep(for: .seconds(2)) diff --git a/ElementX/Sources/Screens/RoomScreen/View/DebugScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemDebugView.swift similarity index 97% rename from ElementX/Sources/Screens/RoomScreen/View/DebugScreen.swift rename to ElementX/Sources/Screens/RoomScreen/View/TimelineItemDebugView.swift index 2117b8a70..412b6f2ad 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/DebugScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemDebugView.swift @@ -16,7 +16,7 @@ import SwiftUI -struct DebugScreen: View { +struct TimelineItemDebugView: View { struct DebugInfo: Identifiable { let id = UUID() let title: String diff --git a/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift index 813aabc2c..a280a2508 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift @@ -52,6 +52,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { self.presentBugReportScreen() case .sessionVerification: self.verifySession() + case .developerOptions: + self.presentDeveloperOptions() case .logout: self.callback?(.logout) } @@ -110,6 +112,11 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { self?.parameters.navigationStackCoordinator?.setSheetCoordinator(nil) } } + + private func presentDeveloperOptions() { + let coordinator = DeveloperOptionsScreenCoordinator() + parameters.navigationStackCoordinator?.push(coordinator) + } private func showSuccess(label: String) { parameters.userIndicatorController?.submitIndicator(UserIndicator(title: label)) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift index 37cceab2d..43ad6ab4e 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift @@ -22,6 +22,7 @@ enum SettingsScreenViewModelAction { case toggleAnalytics case reportBug case sessionVerification + case developerOptions case logout } @@ -32,6 +33,7 @@ struct SettingsScreenViewState: BindableState { var userAvatarURL: URL? var userDisplayName: String? var showSessionVerificationSection: Bool + var showDeveloperOptions: Bool } struct SettingsScreenViewStateBindings { @@ -45,4 +47,5 @@ enum SettingsScreenViewAction { case sessionVerification case logout case changedTimelineStyle + case developerOptions } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift index 689243e70..82d22bb42 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift @@ -23,14 +23,15 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo private let userSession: UserSessionProtocol var callback: ((SettingsScreenViewModelAction) -> Void)? - + init(withUserSession userSession: UserSessionProtocol) { self.userSession = userSession let bindings = SettingsScreenViewStateBindings(timelineStyle: ServiceLocator.shared.settings.timelineStyle) super.init(initialViewState: .init(bindings: bindings, deviceID: userSession.deviceId, userID: userSession.userID, - showSessionVerificationSection: !(userSession.sessionVerificationController?.isVerified ?? false)), + showSessionVerificationSection: !(userSession.sessionVerificationController?.isVerified ?? false), + showDeveloperOptions: ServiceLocator.shared.settings.canShowDeveloperOptions), imageProvider: userSession.mediaProvider) ServiceLocator.shared.settings.$timelineStyle @@ -78,6 +79,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo callback?(.sessionVerification) case .changedTimelineStyle: ServiceLocator.shared.settings.timelineStyle = state.bindings.timelineStyle + case .developerOptions: + callback?(.developerOptions) } } } diff --git a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift index 96b8c8756..dc61df767 100644 --- a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift @@ -18,7 +18,6 @@ import SwiftUI struct SettingsScreen: View { @State private var showingLogoutConfirmation = false - @Environment(\.colorScheme) private var colorScheme @ScaledMetric private var avatarSize = AvatarSize.user(on: .settings).value @ScaledMetric private var menuIconSize = 30.0 @@ -40,6 +39,11 @@ struct SettingsScreen: View { simplifiedSection .listRowBackground(Color.element.formRowBackground) + if context.viewState.showDeveloperOptions { + developerOptionsSection + .listRowBackground(Color.element.formRowBackground) + } + signOutSection .listRowBackground(Color.element.formRowBackground) } @@ -89,6 +93,16 @@ struct SettingsScreen: View { } } + private var developerOptionsSection: some View { + Section { + SettingsDefaultRow(title: ElementL10n.settingsDeveloperOptions, + image: Image(systemName: "hammer.circle")) { + context.send(viewAction: .developerOptions) + } + .accessibilityIdentifier("sessionVerificationButton") + } + } + private var simplifiedSection: some View { Section { SettingsPickerRow(title: ElementL10n.settingsTimelineStyle, diff --git a/UITests/Sources/DeveloperOptionsScreenScreenUITests.swift b/UITests/Sources/DeveloperOptionsScreenScreenUITests.swift new file mode 100644 index 000000000..a823f3c56 --- /dev/null +++ b/UITests/Sources/DeveloperOptionsScreenScreenUITests.swift @@ -0,0 +1,20 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import ElementX +import XCTest + +class DeveloperOptionsScreenScreenUITests: XCTestCase { } diff --git a/UnitTests/Sources/DeveloperOptionsScreenViewModelTests.swift b/UnitTests/Sources/DeveloperOptionsScreenViewModelTests.swift new file mode 100644 index 000000000..802b3967e --- /dev/null +++ b/UnitTests/Sources/DeveloperOptionsScreenViewModelTests.swift @@ -0,0 +1,22 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +@MainActor +class DeveloperOptionsScreenViewModelTests: XCTestCase { }