From fa5053b5cc8129675865f1b2360219c4c49cf8dd Mon Sep 17 00:00:00 2001 From: David Perez Date: Mon, 22 Sep 2025 15:30:25 -0500 Subject: [PATCH] Add empty state for debug menu without feature flags (#5918) --- .../feature/debugmenu/DebugMenuScreen.kt | 32 ++++++++-------- .../components/FeatureFlagListItems.kt | 6 +-- .../feature/debugmenu/DebugMenuScreenTest.kt | 37 +++++++++++-------- .../feature/debugmenu/DebugMenuScreen.kt | 22 ++++++----- .../feature/debugmenu/DebugMenuViewModel.kt | 9 +++-- .../components/FeatureFlagListItems.kt | 2 +- .../feature/debugmenu/DebugMenuScreenTest.kt | 36 ++++++++---------- .../debugmenu/DebugMenuViewModelTest.kt | 6 ++- 8 files changed, 79 insertions(+), 71 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt index 735a754e8b..c73e4d99a4 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt @@ -84,22 +84,22 @@ fun DebugMenuScreen( modifier = Modifier.verticalScroll(rememberScrollState()), ) { Spacer(modifier = Modifier.height(height = 12.dp)) - FeatureFlagContent( - featureFlagMap = state.featureFlags, - onValueChange = remember(viewModel) { - { key, value -> - viewModel.trySendAction(DebugMenuAction.UpdateFeatureFlag(key, value)) - } - }, - onResetValues = remember(viewModel) { - { - viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues) - } - }, - ) - Spacer(Modifier.height(height = 16.dp)) - BitwardenHorizontalDivider() - Spacer(Modifier.height(height = 16.dp)) + if (state.featureFlags.isNotEmpty()) { + FeatureFlagContent( + featureFlagMap = state.featureFlags, + onValueChange = remember(viewModel) { + { key, value -> + viewModel.trySendAction(DebugMenuAction.UpdateFeatureFlag(key, value)) + } + }, + onResetValues = remember(viewModel) { + { viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues) } + }, + ) + Spacer(Modifier.height(height = 16.dp)) + BitwardenHorizontalDivider() + Spacer(Modifier.height(height = 16.dp)) + } OnboardingOverrideContent( onStartOnboarding = remember(viewModel) { { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt index 6a5c86eb74..9aae60fc98 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt @@ -18,13 +18,11 @@ fun FlagKey.ListItemContent( cardStyle: CardStyle, modifier: Modifier = Modifier, ) = when (val flagKey = this) { - FlagKey.DummyBoolean, is FlagKey.DummyInt, FlagKey.DummyString, - -> { - Unit - } + -> Unit + FlagKey.DummyBoolean, FlagKey.BitwardenAuthenticationEnabled, FlagKey.CredentialExchangeProtocolImport, FlagKey.CredentialExchangeProtocolExport, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt index 3f85604bfe..f4ac4dd941 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt @@ -13,6 +13,7 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -20,9 +21,7 @@ import org.junit.Test class DebugMenuScreenTest : BitwardenComposeTest() { private var onNavigateBackCalled = false private val mutableEventFlow = bufferedMutableSharedFlow() - private val mutableStateFlow = MutableStateFlow( - value = DebugMenuState(featureFlags = persistentMapOf()), - ) + private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val viewModel = mockk(relaxed = true) { every { stateFlow } returns mutableStateFlow every { eventFlow } returns mutableEventFlow @@ -75,43 +74,43 @@ class DebugMenuScreenTest : BitwardenComposeTest() { @Test fun `feature flag content should not display if the state is empty`() { + mutableStateFlow.update { DebugMenuState(featureFlags = persistentMapOf()) } composeTestRule - .onNodeWithText("Email Verification", ignoreCase = true) + .onNodeWithText(text = "dummy-boolean") .assertDoesNotExist() } @Test fun `feature flag content should display if the state is not empty`() { - mutableStateFlow.tryEmit( + mutableStateFlow.update { DebugMenuState( featureFlags = persistentMapOf( - FlagKey.CredentialExchangeProtocolImport to true, + FlagKey.DummyBoolean to true, ), - ), - ) - + ) + } composeTestRule - .onNodeWithText("CXP Import", ignoreCase = true) + .onNodeWithText(text = "dummy-boolean", ignoreCase = true) .assertExists() } @Test fun `boolean feature flag content should send action when clicked`() { - mutableStateFlow.tryEmit( + mutableStateFlow.update { DebugMenuState( featureFlags = persistentMapOf( - FlagKey.CredentialExchangeProtocolImport to true, + FlagKey.DummyBoolean to true, ), - ), - ) + ) + } composeTestRule - .onNodeWithText("CXP Import", ignoreCase = true) + .onNodeWithText(text = "dummy-boolean") .performClick() verify(exactly = 1) { viewModel.trySendAction( DebugMenuAction.UpdateFeatureFlag( - FlagKey.CredentialExchangeProtocolImport, + FlagKey.DummyBoolean, false, ), ) @@ -160,3 +159,9 @@ class DebugMenuScreenTest : BitwardenComposeTest() { verify(exactly = 1) { viewModel.trySendAction(DebugMenuAction.ResetCoachMarkTourStatuses) } } } + +private val DEFAULT_STATE: DebugMenuState = DebugMenuState( + featureFlags = persistentMapOf( + FlagKey.DummyBoolean to true, + ), +) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuScreen.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuScreen.kt index 1b23644ec1..120ad7c86f 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuScreen.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuScreen.kt @@ -23,6 +23,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledButton +import com.bitwarden.authenticator.ui.platform.components.content.AuthenticatorErrorContent import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold import com.bitwarden.authenticator.ui.platform.feature.debugmenu.components.ListItemContent @@ -74,12 +75,14 @@ fun DebugMenuScreen( ) }, ) { innerPadding -> - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(innerPadding), - ) { - Spacer(modifier = Modifier.height(16.dp)) + if (state.featureFlags.isEmpty()) { + AuthenticatorErrorContent( + message = stringResource(id = BitwardenString.empty_item_list), + modifier = Modifier + .padding(paddingValues = innerPadding) + .fillMaxSize(), + ) + } else { FeatureFlagContent( featureFlagMap = state.featureFlags, onValueChange = remember(viewModel) { @@ -88,10 +91,11 @@ fun DebugMenuScreen( } }, onResetValues = remember(viewModel) { - { - viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues) - } + { viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues) } }, + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(paddingValues = innerPadding), ) } } diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuViewModel.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuViewModel.kt index 57d9b4ab50..9b352718db 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuViewModel.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuViewModel.kt @@ -6,6 +6,9 @@ import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.ui.platform.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.Job import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -23,7 +26,7 @@ class DebugMenuViewModel @Inject constructor( featureFlagManager: FeatureFlagManager, private val debugMenuRepository: DebugMenuRepository, ) : BaseViewModel( - initialState = DebugMenuState(featureFlags = emptyMap()), + initialState = DebugMenuState(featureFlags = persistentMapOf()), ) { private var featureFlagResetJob: Job? = null @@ -60,7 +63,7 @@ class DebugMenuViewModel @Inject constructor( private fun handleUpdateFeatureFlagMap(action: DebugMenuAction.Internal.UpdateFeatureFlagMap) { mutableStateFlow.update { - it.copy(featureFlags = action.newMap) + it.copy(featureFlags = action.newMap.toImmutableMap()) } } @@ -73,7 +76,7 @@ class DebugMenuViewModel @Inject constructor( * State for the [DebugMenuViewModel] */ data class DebugMenuState( - val featureFlags: Map, Any>, + val featureFlags: ImmutableMap, Any>, ) /** diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt index 296f5cfbef..7305734957 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt @@ -17,11 +17,11 @@ fun FlagKey.ListItemContent( onValueChange: (key: FlagKey, value: T) -> Unit, modifier: Modifier = Modifier, ) = when (val flagKey = this) { - FlagKey.DummyBoolean, is FlagKey.DummyInt, FlagKey.DummyString, -> Unit + FlagKey.DummyBoolean, FlagKey.BitwardenAuthenticationEnabled, FlagKey.CipherKeyEncryption, FlagKey.CredentialExchangeProtocolExport, diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt index c2199ef1ca..aa2238a634 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt @@ -10,7 +10,9 @@ import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -18,7 +20,7 @@ import org.junit.Test class DebugMenuScreenTest : AuthenticatorComposeTest() { private var onNavigateBackCalled = false private val mutableEventFlow = bufferedMutableSharedFlow() - private val mutableStateFlow = MutableStateFlow(DebugMenuState(featureFlags = emptyMap())) + private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val viewModel = mockk(relaxed = true) { every { stateFlow } returns mutableStateFlow every { eventFlow } returns mutableEventFlow @@ -51,43 +53,31 @@ class DebugMenuScreenTest : AuthenticatorComposeTest() { @Test fun `feature flag content should not display if the state is empty`() { + mutableStateFlow.update { + DebugMenuState(featureFlags = persistentMapOf()) + } composeTestRule - .onNodeWithText("Bitwarden authentication enabled", ignoreCase = true) + .onNodeWithText(text = "dummy-boolean") .assertDoesNotExist() } @Test fun `feature flag content should display if the state is not empty`() { - mutableStateFlow.tryEmit( - DebugMenuState( - featureFlags = mapOf( - FlagKey.BitwardenAuthenticationEnabled to true, - ), - ), - ) - composeTestRule - .onNodeWithText("Bitwarden authentication enabled", ignoreCase = true) + .onNodeWithText(text = "dummy-boolean") .assertExists() } @Test fun `boolean feature flag content should send action when clicked`() { - mutableStateFlow.tryEmit( - DebugMenuState( - featureFlags = mapOf( - FlagKey.BitwardenAuthenticationEnabled to true, - ), - ), - ) composeTestRule - .onNodeWithText("Bitwarden authentication enabled", ignoreCase = true) + .onNodeWithText(text = "dummy-boolean") .performClick() verify { viewModel.trySendAction( DebugMenuAction.UpdateFeatureFlag( - FlagKey.BitwardenAuthenticationEnabled, + FlagKey.DummyBoolean, false, ), ) @@ -104,3 +94,9 @@ class DebugMenuScreenTest : AuthenticatorComposeTest() { verify { viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues) } } } + +private val DEFAULT_STATE: DebugMenuState = DebugMenuState( + featureFlags = persistentMapOf( + FlagKey.DummyBoolean to true, + ), +) diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt index f48849a3ce..32301ea822 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt @@ -12,6 +12,8 @@ import io.mockk.just import io.mockk.mockk import io.mockk.runs import io.mockk.verify +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -77,11 +79,11 @@ class DebugMenuViewModelTest : BaseViewModelTest() { ) } -private val DEFAULT_MAP_VALUE: Map, Any> = mapOf( +private val DEFAULT_MAP_VALUE: ImmutableMap, Any> = persistentMapOf( FlagKey.BitwardenAuthenticationEnabled to true, ) -private val UPDATED_MAP_VALUE: Map, Any> = mapOf( +private val UPDATED_MAP_VALUE: ImmutableMap, Any> = persistentMapOf( FlagKey.BitwardenAuthenticationEnabled to false, )