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.asText
import com.bitwarden.ui.util.concat import com.bitwarden.ui.util.concat
import com.x8bit.bitwarden.R 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.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
@ -65,6 +66,7 @@ private const val MAX_FILE_SIZE_BYTES: Long = 100 * 1024 * 1024
@HiltViewModel @HiltViewModel
class AddEditSendViewModel @Inject constructor( class AddEditSendViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
authRepo: AuthRepository,
private val clock: Clock, private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager, private val clipboardManager: BitwardenClipboardManager,
private val environmentRepo: EnvironmentRepository, private val environmentRepo: EnvironmentRepository,
@ -131,6 +133,7 @@ class AddEditSendViewModel @Inject constructor(
policyDisablesSend = policyManager policyDisablesSend = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) .getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
.any(), .any(),
isPremium = authRepo.userStateFlow.value?.activeAccount?.isPremium == true,
) )
}, },
) { ) {
@ -499,6 +502,19 @@ class AddEditSendViewModel @Inject constructor(
} }
(content.selectedType as? AddEditSendState.ViewState.Content.SendType.File) (content.selectedType as? AddEditSendState.ViewState.Content.SendType.File)
?.let { fileType -> ?.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()) { if (fileType.name.isNullOrBlank()) {
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
@ -686,6 +702,7 @@ data class AddEditSendState(
val isShared: Boolean, val isShared: Boolean,
val baseWebSendUrl: String, val baseWebSendUrl: String,
val policyDisablesSend: Boolean, val policyDisablesSend: Boolean,
val isPremium: Boolean,
) : Parcelable { ) : Parcelable {
/** /**

View File

@ -838,15 +838,30 @@ class VaultItemListingViewModel @Inject constructor(
} }
is VaultItemListingState.ItemListingType.Send -> { is VaultItemListingState.ItemListingType.Send -> {
sendEvent( when (val sendType = itemListingType.toSendItemType()) {
VaultItemListingEvent.NavigateToAddSendItem( SendItemType.FILE -> {
sendType = itemListingType.toSendItemType(), 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))
}
}
}
}
}
private fun handleViewSendClick(action: ListingItemOverflowAction.SendAction.ViewClick) { private fun handleViewSendClick(action: ListingItemOverflowAction.SendAction.ViewClick) {
sendEvent( sendEvent(
VaultItemListingEvent.NavigateToViewSendItem( VaultItemListingEvent.NavigateToViewSendItem(

View File

@ -881,4 +881,5 @@ private val DEFAULT_STATE = AddEditSendState(
baseWebSendUrl = "https://vault.bitwarden.com/#/send/", baseWebSendUrl = "https://vault.bitwarden.com/#/send/",
policyDisablesSend = false, policyDisablesSend = false,
sendType = SendItemType.TEXT, 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.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.R 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.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.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager 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.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
@ -66,6 +70,10 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
private val clipboardManager: BitwardenClipboardManager = mockk { private val clipboardManager: BitwardenClipboardManager = mockk {
every { setText(any<String>(), toastDescriptorOverride = any<Text>()) } just runs 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 { private val environmentRepository: EnvironmentRepository = mockk {
every { environment } returns Environment.Us 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 @Test
fun `SaveClick with file missing should show error dialog`() { fun `SaveClick with file missing should show error dialog`() {
val initialState = DEFAULT_STATE.copy( val initialState = DEFAULT_STATE.copy(
@ -966,6 +1006,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
toAddEditSendArgs() toAddEditSendArgs()
} returns AddEditSendArgs(sendType = sendType, addEditSendType = addEditSendType) } returns AddEditSendArgs(sendType = sendType, addEditSendType = addEditSendType)
}, },
authRepo = authRepository,
environmentRepo = environmentRepository, environmentRepo = environmentRepository,
specialCircumstanceManager = specialCircumstanceManager, specialCircumstanceManager = specialCircumstanceManager,
clock = clock, clock = clock,
@ -1013,4 +1054,30 @@ private val DEFAULT_STATE = AddEditSendState(
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL, baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
policyDisablesSend = false, policyDisablesSend = false,
sendType = SendItemType.TEXT, 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 @Test
fun `AddVaultItemClick for send item should emit NavigateToAddVaultItem`() = runTest { fun `AddVaultItemClick for text send item should emit NavigateToAddVaultItem`() = runTest {
val viewModel = createVaultItemListingViewModel( val viewModel = createVaultItemListingViewModel(
createSavedStateHandleWithVaultItemListingType(VaultItemListingType.SendText), 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 @Test
fun `ItemTypeToAddSelected sends NavigateToAddFolder for folder selection`() = runTest { fun `ItemTypeToAddSelected sends NavigateToAddFolder for folder selection`() = runTest {
val viewModel = createVaultItemListingViewModel( val viewModel = createVaultItemListingViewModel(
@ -5331,6 +5372,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
itemListingType: VaultItemListingState.ItemListingType = VaultItemListingState.ItemListingType.Vault.Login, itemListingType: VaultItemListingState.ItemListingType = VaultItemListingState.ItemListingType.Vault.Login,
viewState: VaultItemListingState.ViewState = VaultItemListingState.ViewState.Loading, viewState: VaultItemListingState.ViewState = VaultItemListingState.ViewState.Loading,
dialogState: VaultItemListingState.DialogState? = null, dialogState: VaultItemListingState.DialogState? = null,
isPremium: Boolean = true,
): VaultItemListingState = ): VaultItemListingState =
VaultItemListingState( VaultItemListingState(
itemListingType = itemListingType, itemListingType = itemListingType,
@ -5348,7 +5390,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
policyDisablesSend = false, policyDisablesSend = false,
hasMasterPassword = true, hasMasterPassword = true,
createCredentialRequest = null, createCredentialRequest = null,
isPremium = true, isPremium = isPremium,
isRefreshing = false, isRefreshing = false,
restrictItemTypesPolicyOrgIds = persistentListOf(), restrictItemTypesPolicyOrgIds = persistentListOf(),
) )