PM-37705: Hide Send navigation when DISABLE_SEND policy is enabled

This commit is contained in:
David Perez
2026-05-19 16:00:12 -05:00
parent e949dd710a
commit d3b26501b1
4 changed files with 79 additions and 3 deletions

View File

@@ -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<NavigationItem>(
val navigationItems = persistentListOfNotNull<NavigationItem>(
VaultUnlockedNavBarTab.Vault(labelRes = state.vaultNavBarLabelRes),
VaultUnlockedNavBarTab.Send,
VaultUnlockedNavBarTab.Send.takeUnless { state.areSendsDisabled },
VaultUnlockedNavBarTab.Generator,
VaultUnlockedNavBarTab.Settings(state.notificationState.settingsTabNotificationCount),
)

View File

@@ -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<VaultUnlockedNavBarState, VaultUnlockedNavBarEvent, VaultUnlockedNavBarAction>(
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()
}
}

View File

@@ -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,
)

View File

@@ -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<List<SyncResponseJson.Policy>>(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,
)