From d3b26501b17ea1e8b9e8c164d02cd2b5967ad0f0 Mon Sep 17 00:00:00 2001 From: David Perez Date: Tue, 19 May 2026 16:00:12 -0500 Subject: [PATCH] PM-37705: Hide Send navigation when DISABLE_SEND policy is enabled --- .../VaultUnlockedNavBarScreen.kt | 6 ++-- .../VaultUnlockedNavBarViewModel.kt | 29 +++++++++++++++ .../VaultUnlockedNavBarScreenTest.kt | 11 ++++++ .../VaultUnlockedNavBarViewModelTest.kt | 36 +++++++++++++++++++ 4 files changed, 79 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index 6f104c4191..b0c85afa17 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -14,6 +14,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.currentBackStackEntryAsState +import com.bitwarden.core.util.persistentListOfNotNull import com.bitwarden.ui.platform.base.util.EventsEffect import com.bitwarden.ui.platform.base.util.navigateToTabOrRoot import com.bitwarden.ui.platform.components.navigation.model.NavigationItem @@ -37,7 +38,6 @@ import com.x8bit.bitwarden.ui.vault.feature.importitems.navigateToImportItemsScr import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs import com.x8bit.bitwarden.ui.vault.feature.vault.VaultGraphRoute import com.x8bit.bitwarden.ui.vault.feature.vault.vaultGraph -import kotlinx.collections.immutable.persistentListOf /** * Top level composable for the Vault Unlocked Screen. @@ -159,9 +159,9 @@ private fun VaultUnlockedNavBarScaffold( // This scaffold will host screens that contain top bars while not hosting one itself. // We need to ignore the all insets here and let the content screens handle it themselves. val navBackStackEntry by navController.currentBackStackEntryAsState() - val navigationItems = persistentListOf( + val navigationItems = persistentListOfNotNull( VaultUnlockedNavBarTab.Vault(labelRes = state.vaultNavBarLabelRes), - VaultUnlockedNavBarTab.Send, + VaultUnlockedNavBarTab.Send.takeUnless { state.areSendsDisabled }, VaultUnlockedNavBarTab.Generator, VaultUnlockedNavBarTab.Settings(state.notificationState.settingsTabNotificationCount), ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt index a370b909aa..9bacb39c3e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt @@ -2,17 +2,20 @@ package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar import androidx.annotation.StringRes import androidx.lifecycle.viewModelScope +import com.bitwarden.network.model.PolicyTypeJson import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.DeferredBackgroundEvent import com.bitwarden.ui.platform.resource.BitwardenString import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager +import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.model.VaultUnlockedNavBarTab import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import javax.inject.Inject @@ -25,12 +28,16 @@ class VaultUnlockedNavBarViewModel @Inject constructor( authRepository: AuthRepository, specialCircumstancesManager: SpecialCircumstanceManager, firstTimeActionManager: FirstTimeActionManager, + policyManager: PolicyManager, ) : BaseViewModel( initialState = VaultUnlockedNavBarState( vaultNavBarLabelRes = BitwardenString.my_vault, notificationState = VaultUnlockedNavBarNotificationState( settingsTabNotificationCount = firstTimeActionManager.allSettingsBadgeCountFlow.value, ), + areSendsDisabled = policyManager + .getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) + .any(), ), ) { init { @@ -48,6 +55,12 @@ class VaultUnlockedNavBarViewModel @Inject constructor( } .launchIn(viewModelScope) + policyManager + .getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND) + .map { VaultUnlockedNavBarAction.Internal.SendPolicyUpdateReceive(it.any()) } + .onEach(::sendAction) + .launchIn(viewModelScope) + when (specialCircumstancesManager.specialCircumstance) { SpecialCircumstance.GeneratorShortcut -> { sendEvent(VaultUnlockedNavBarEvent.Shortcut.NavigateToGeneratorScreen) @@ -111,6 +124,10 @@ class VaultUnlockedNavBarViewModel @Inject constructor( is VaultUnlockedNavBarAction.Internal.SettingsNotificationCountUpdate -> { handleSettingsNotificationCountUpdate(action) } + + is VaultUnlockedNavBarAction.Internal.SendPolicyUpdateReceive -> { + handleSendPolicyUpdateReceive(action) + } } } // #region BottomTabViewModel Action Handlers @@ -175,6 +192,12 @@ class VaultUnlockedNavBarViewModel @Inject constructor( ) } } + + private fun handleSendPolicyUpdateReceive( + action: VaultUnlockedNavBarAction.Internal.SendPolicyUpdateReceive, + ) { + mutableStateFlow.update { it.copy(areSendsDisabled = action.hasPolicy) } + } // #endregion BottomTabViewModel Action Handlers } @@ -184,6 +207,7 @@ class VaultUnlockedNavBarViewModel @Inject constructor( data class VaultUnlockedNavBarState( @field:StringRes val vaultNavBarLabelRes: Int, val notificationState: VaultUnlockedNavBarNotificationState, + val areSendsDisabled: Boolean, ) /** @@ -230,6 +254,11 @@ sealed class VaultUnlockedNavBarAction { * Indicates a change to the count of settings notifications to show */ data class SettingsNotificationCountUpdate(val count: Int) : Internal() + + /** + * Indicates a change to the count of settings notifications to show + */ + data class SendPolicyUpdateReceive(val hasPolicy: Boolean) : Internal() } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt index f272a0573f..e3c64110bd 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt @@ -193,6 +193,7 @@ class VaultUnlockedNavBarScreenTest : BitwardenComposeTest() { notificationState = VaultUnlockedNavBarNotificationState( settingsTabNotificationCount = 0, ), + areSendsDisabled = false, ), ) @@ -200,6 +201,15 @@ class VaultUnlockedNavBarScreenTest : BitwardenComposeTest() { composeTestRule.onNodeWithText(text = "Vaults").assertExists() } + @Test + fun `send tab should be hidden when areSendsDisabled is true`() { + composeTestRule.onNodeWithText(text = "Send").assertExists() + + mutableStateFlow.update { it.copy(areSendsDisabled = true) } + + composeTestRule.onNodeWithText(text = "Send").assertDoesNotExist() + } + @Suppress("MaxLineLength") @Test fun `settings tab notification count should update according to state and show correct count`() { @@ -232,4 +242,5 @@ private val DEFAULT_STATE = VaultUnlockedNavBarState( notificationState = VaultUnlockedNavBarNotificationState( settingsTabNotificationCount = 0, ), + areSendsDisabled = false, ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt index 1f7b27b16f..6722722a25 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt @@ -1,11 +1,15 @@ package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar import app.cash.turbine.test +import com.bitwarden.network.model.PolicyTypeJson +import com.bitwarden.network.model.SyncResponseJson +import com.bitwarden.network.model.createMockPolicy import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.resource.BitwardenString import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager +import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import io.mockk.every @@ -32,6 +36,16 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { private val firstTimeActionManager: FirstTimeActionManager = mockk { every { allSettingsBadgeCountFlow } returns mutableSettingsBadgeCountFlow } + private val mutableDisableSendsPolicyFlow = + MutableStateFlow>(emptyList()) + private val policyManager: PolicyManager = mockk { + every { + getActivePoliciesFlow(PolicyTypeJson.DISABLE_SEND) + } returns mutableDisableSendsPolicyFlow + every { + getActivePolicies(PolicyTypeJson.DISABLE_SEND) + } answers { mutableDisableSendsPolicyFlow.value } + } @Suppress("MaxLineLength") @Test @@ -127,6 +141,7 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { val expectedWithOrganizations = VaultUnlockedNavBarState( vaultNavBarLabelRes = BitwardenString.vaults, notificationState = DEFAULT_NOTIFICATION_STATE, + areSendsDisabled = false, ) val accountWithoutOrganizations: UserState.Account = mockk { every { userId } returns activeUserId @@ -135,6 +150,7 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { val expectedWithoutOrganizations = VaultUnlockedNavBarState( vaultNavBarLabelRes = BitwardenString.my_vault, notificationState = DEFAULT_NOTIFICATION_STATE, + areSendsDisabled = false, ) val viewModel = createViewModel() @@ -311,11 +327,30 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `DISABLE_SEND policy flow update with disabled policy should set areSendsDisabled to false`() = + runTest { + val viewModel = createViewModel() + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE.copy(areSendsDisabled = false), awaitItem()) + + mutableDisableSendsPolicyFlow.emit( + listOf(createMockPolicy(type = PolicyTypeJson.DISABLE_SEND)), + ) + assertEquals(DEFAULT_STATE.copy(areSendsDisabled = true), awaitItem()) + + mutableDisableSendsPolicyFlow.emit(emptyList()) + assertEquals(DEFAULT_STATE.copy(areSendsDisabled = false), awaitItem()) + } + } + private fun createViewModel() = VaultUnlockedNavBarViewModel( authRepository = authRepository, specialCircumstancesManager = specialCircumstancesManager, firstTimeActionManager = firstTimeActionManager, + policyManager = policyManager, ) } @@ -326,4 +361,5 @@ private val DEFAULT_NOTIFICATION_STATE = VaultUnlockedNavBarNotificationState( private val DEFAULT_STATE = VaultUnlockedNavBarState( vaultNavBarLabelRes = BitwardenString.my_vault, notificationState = DEFAULT_NOTIFICATION_STATE, + areSendsDisabled = false, )