PM-23910: Disallow file sends for non-premium users (#5544)

This commit is contained in:
David Perez 2025-07-18 15:44:52 -05:00 committed by GitHub
parent 25680f9255
commit e75d7844de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 149 additions and 7 deletions

View File

@ -15,6 +15,7 @@ import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.concat
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
@ -65,6 +66,7 @@ private const val MAX_FILE_SIZE_BYTES: Long = 100 * 1024 * 1024
@HiltViewModel
class AddEditSendViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
authRepo: AuthRepository,
private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager,
private val environmentRepo: EnvironmentRepository,
@ -131,6 +133,7 @@ class AddEditSendViewModel @Inject constructor(
policyDisablesSend = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
.any(),
isPremium = authRepo.userStateFlow.value?.activeAccount?.isPremium == true,
)
},
) {
@ -499,6 +502,19 @@ class AddEditSendViewModel @Inject constructor(
}
(content.selectedType as? AddEditSendState.ViewState.Content.SendType.File)
?.let { fileType ->
if (!state.isPremium) {
// We should never get here without a premium account, but we do one last
// check just in case.
mutableStateFlow.update {
it.copy(
dialogState = AddEditSendState.DialogState.Error(
title = R.string.send.asText(),
message = R.string.send_file_premium_required.asText(),
),
)
}
return@onContent
}
if (fileType.name.isNullOrBlank()) {
mutableStateFlow.update {
it.copy(
@ -686,6 +702,7 @@ data class AddEditSendState(
val isShared: Boolean,
val baseWebSendUrl: String,
val policyDisablesSend: Boolean,
val isPremium: Boolean,
) : Parcelable {
/**

View File

@ -838,11 +838,26 @@ class VaultItemListingViewModel @Inject constructor(
}
is VaultItemListingState.ItemListingType.Send -> {
sendEvent(
VaultItemListingEvent.NavigateToAddSendItem(
sendType = itemListingType.toSendItemType(),
),
)
when (val sendType = itemListingType.toSendItemType()) {
SendItemType.FILE -> {
if (state.isPremium) {
sendEvent(VaultItemListingEvent.NavigateToAddSendItem(sendType))
} else {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Error(
title = R.string.send.asText(),
message = R.string.send_file_premium_required.asText(),
),
)
}
}
}
SendItemType.TEXT -> {
sendEvent(VaultItemListingEvent.NavigateToAddSendItem(sendType))
}
}
}
}
}

View File

@ -881,4 +881,5 @@ private val DEFAULT_STATE = AddEditSendState(
baseWebSendUrl = "https://vault.bitwarden.com/#/send/",
policyDisablesSend = false,
sendType = SendItemType.TEXT,
isPremium = true,
)

View File

@ -12,10 +12,14 @@ import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
@ -66,6 +70,10 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
private val clipboardManager: BitwardenClipboardManager = mockk {
every { setText(any<String>(), toastDescriptorOverride = any<Text>()) } just runs
}
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
private val authRepository = mockk<AuthRepository> {
every { userStateFlow } returns mutableUserStateFlow
}
private val environmentRepository: EnvironmentRepository = mockk {
every { environment } returns Environment.Us
}
@ -386,6 +394,38 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `SaveClick for file without premium show error dialog`() {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(DEFAULT_ACCOUNT.copy(isPremium = false)),
)
val initialState = DEFAULT_STATE.copy(
isPremium = false,
viewState = DEFAULT_VIEW_STATE.copy(
common = DEFAULT_COMMON_STATE.copy(name = "test"),
selectedType = AddEditSendState.ViewState.Content.SendType.File(
uri = null,
name = null,
displaySize = null,
sizeBytes = null,
),
),
)
val viewModel = createViewModel(initialState)
viewModel.trySendAction(AddEditSendAction.SaveClick)
assertEquals(
initialState.copy(
dialogState = AddEditSendState.DialogState.Error(
title = R.string.send.asText(),
message = R.string.send_file_premium_required.asText(),
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `SaveClick with file missing should show error dialog`() {
val initialState = DEFAULT_STATE.copy(
@ -966,6 +1006,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
toAddEditSendArgs()
} returns AddEditSendArgs(sendType = sendType, addEditSendType = addEditSendType)
},
authRepo = authRepository,
environmentRepo = environmentRepository,
specialCircumstanceManager = specialCircumstanceManager,
clock = clock,
@ -1013,4 +1054,30 @@ private val DEFAULT_STATE = AddEditSendState(
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
policyDisablesSend = false,
sendType = SendItemType.TEXT,
isPremium = true,
)
private val DEFAULT_ACCOUNT = UserState.Account(
userId = "activeUserId",
name = "Active User",
email = "active@bitwarden.com",
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
isBiometricsEnabled = false,
organizations = emptyList(),
needsMasterPassword = false,
trustedDevice = null,
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = FirstTimeState(showImportLoginsCard = true),
)
private val DEFAULT_USER_STATE = UserState(
activeUserId = "activeUserId",
accounts = listOf(DEFAULT_ACCOUNT),
)

View File

@ -1422,7 +1422,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
@Test
fun `AddVaultItemClick for send item should emit NavigateToAddVaultItem`() = runTest {
fun `AddVaultItemClick for text send item should emit NavigateToAddVaultItem`() = runTest {
val viewModel = createVaultItemListingViewModel(
createSavedStateHandleWithVaultItemListingType(VaultItemListingType.SendText),
)
@ -1435,6 +1435,47 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `AddVaultItemClick for file send item with premium should emit NavigateToAddVaultItem`() =
runTest {
val viewModel = createVaultItemListingViewModel(
createSavedStateHandleWithVaultItemListingType(VaultItemListingType.SendFile),
)
viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
assertEquals(
VaultItemListingEvent.NavigateToAddSendItem(sendType = SendItemType.FILE),
awaitItem(),
)
}
}
@Test
fun `AddVaultItemClick for file send item without premium should display error dialog`() =
runTest {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(DEFAULT_ACCOUNT.copy(isPremium = false)),
)
val viewModel = createVaultItemListingViewModel(
createSavedStateHandleWithVaultItemListingType(VaultItemListingType.SendFile),
)
viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
expectNoEvents()
}
assertEquals(
createVaultItemListingState(
itemListingType = VaultItemListingState.ItemListingType.Send.SendFile,
dialogState = VaultItemListingState.DialogState.Error(
title = R.string.send.asText(),
message = R.string.send_file_premium_required.asText(),
),
isPremium = false,
),
viewModel.stateFlow.value,
)
}
@Test
fun `ItemTypeToAddSelected sends NavigateToAddFolder for folder selection`() = runTest {
val viewModel = createVaultItemListingViewModel(
@ -5331,6 +5372,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
itemListingType: VaultItemListingState.ItemListingType = VaultItemListingState.ItemListingType.Vault.Login,
viewState: VaultItemListingState.ViewState = VaultItemListingState.ViewState.Loading,
dialogState: VaultItemListingState.DialogState? = null,
isPremium: Boolean = true,
): VaultItemListingState =
VaultItemListingState(
itemListingType = itemListingType,
@ -5348,7 +5390,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
policyDisablesSend = false,
hasMasterPassword = true,
createCredentialRequest = null,
isPremium = true,
isPremium = isPremium,
isRefreshing = false,
restrictItemTypesPolicyOrgIds = persistentListOf(),
)