[PM-37284] fix: Show Upgraded to Premium card across all Send view states (#6947)

This commit is contained in:
Patrick Honkonen
2026-05-20 14:00:32 -04:00
committed by GitHub
parent 8940a2c490
commit 9e27b950e8
5 changed files with 121 additions and 32 deletions

View File

@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@@ -15,17 +14,15 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenGroupItem
import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers
import com.x8bit.bitwarden.ui.tools.feature.send.model.UpgradedToPremiumCardData
private const val SEND_TYPES_COUNT: Int = 2
@@ -37,39 +34,24 @@ private const val SEND_TYPES_COUNT: Int = 2
fun SendContent(
policyDisablesSend: Boolean,
state: SendState.ViewState.Content,
isUpgradedToPremiumCardEligible: Boolean,
upgradedToPremiumCardData: UpgradedToPremiumCardData?,
sendHandlers: SendHandlers,
onUpgradedToPremiumCardClick: () -> Unit,
onUpgradedToPremiumCardDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
}
if (isUpgradedToPremiumCardEligible) {
upgradedToPremiumCardData?.let {
item {
BitwardenActionCard(
cardTitle = stringResource(id = BitwardenString.upgraded_to_premium),
cardSubtitle = stringResource(
id = BitwardenString.you_now_have_access_to_all_advanced_security_features,
),
actionText = stringResource(id = BitwardenString.learn_more),
isExternalLink = true,
leadingContent = {
Icon(
painter = rememberVectorPainter(id = BitwardenDrawable.ic_star),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.secondary,
)
},
onActionClick = onUpgradedToPremiumCardClick,
onDismissClick = onUpgradedToPremiumCardDismiss,
UpgradedToPremiumActionCard(
onActionClick = it.onCardClick,
onDismissClick = it.onCardDismiss,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
if (policyDisablesSend) {

View File

@@ -31,6 +31,7 @@ import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.tools.feature.send.model.UpgradedToPremiumCardData
/**
* Content for the empty state of the [SendScreen].
@@ -40,12 +41,23 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
fun SendEmpty(
policyDisablesSend: Boolean,
onAddItemClick: () -> Unit,
upgradedToPremiumCardData: UpgradedToPremiumCardData?,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.verticalScroll(rememberScrollState()),
) {
upgradedToPremiumCardData?.let {
Spacer(modifier = Modifier.height(height = 12.dp))
UpgradedToPremiumActionCard(
onActionClick = it.onCardClick,
onDismissClick = it.onCardDismiss,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
if (policyDisablesSend) {
Spacer(modifier = Modifier.height(12.dp))
BitwardenInfoCalloutCard(
@@ -119,6 +131,7 @@ private fun SendEmpty_preview() {
SendEmpty(
policyDisablesSend = false,
onAddItemClick = {},
upgradedToPremiumCardData = null,
)
}
}
@@ -135,6 +148,7 @@ private fun SendEmptyPolicyDisabled_preview() {
SendEmpty(
policyDisablesSend = true,
onAddItemClick = {},
upgradedToPremiumCardData = null,
)
}
}

View File

@@ -5,6 +5,7 @@ import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
@@ -23,6 +24,7 @@ import com.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActio
import com.bitwarden.ui.platform.components.appbar.action.BitwardenSearchActionItem
import com.bitwarden.ui.platform.components.appbar.model.OverflowMenuItemData
import com.bitwarden.ui.platform.components.button.model.BitwardenButtonData
import com.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
@@ -39,6 +41,7 @@ import com.bitwarden.ui.platform.composition.LocalIntentManager
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
@@ -49,6 +52,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendRoute
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.ModeType
import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import com.x8bit.bitwarden.ui.tools.feature.send.model.UpgradedToPremiumCardData
import com.x8bit.bitwarden.ui.tools.feature.send.util.selectionText
import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.ViewSendRoute
import kotlinx.collections.immutable.persistentListOf
@@ -181,24 +185,27 @@ fun SendScreen(
snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) },
) {
val contentModifier = Modifier.fillMaxSize()
val upgradedToPremiumCardData = UpgradedToPremiumCardData(
onCardClick = {
viewModel.trySendAction(SendAction.UpgradedToPremiumCardClick)
},
onCardDismiss = {
viewModel.trySendAction(SendAction.UpgradedToPremiumCardDismiss)
},
).takeIf { state.isUpgradedToPremiumCardEligible }
when (val viewState = state.viewState) {
is SendState.ViewState.Content -> SendContent(
policyDisablesSend = state.policyDisablesSend,
state = viewState,
isUpgradedToPremiumCardEligible = state.isUpgradedToPremiumCardEligible,
upgradedToPremiumCardData = upgradedToPremiumCardData,
sendHandlers = sendHandlers,
onUpgradedToPremiumCardClick = {
viewModel.trySendAction(SendAction.UpgradedToPremiumCardClick)
},
onUpgradedToPremiumCardDismiss = {
viewModel.trySendAction(SendAction.UpgradedToPremiumCardDismiss)
},
modifier = contentModifier,
)
SendState.ViewState.Empty -> SendEmpty(
policyDisablesSend = state.policyDisablesSend,
onAddItemClick = { viewModel.trySendAction(SendAction.AddSendClick) },
upgradedToPremiumCardData = upgradedToPremiumCardData,
modifier = contentModifier,
)
@@ -251,3 +258,33 @@ private fun SendDialogs(
null -> Unit
}
}
/**
* Action card rendered at the top of the Send list when the user has just upgraded to premium.
* Owned by the screen so it can be hosted inside each view state's scrollable container.
*/
@Composable
internal fun UpgradedToPremiumActionCard(
onActionClick: () -> Unit,
onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BitwardenActionCard(
cardTitle = stringResource(id = BitwardenString.upgraded_to_premium),
cardSubtitle = stringResource(
id = BitwardenString.you_now_have_access_to_all_advanced_security_features,
),
actionText = stringResource(id = BitwardenString.learn_more),
leadingContent = {
Icon(
painter = rememberVectorPainter(id = BitwardenDrawable.ic_star),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.secondary,
)
},
isExternalLink = true,
onActionClick = onActionClick,
onDismissClick = onDismissClick,
modifier = modifier,
)
}

View File

@@ -0,0 +1,11 @@
package com.x8bit.bitwarden.ui.tools.feature.send.model
/**
* Handlers for the "Upgraded to Premium" action card shown on Send surfaces.
*
* Pass `null` to indicate the card should not be displayed.
*/
data class UpgradedToPremiumCardData(
val onCardClick: () -> Unit,
val onCardDismiss: () -> Unit,
)

View File

@@ -1000,6 +1000,51 @@ class SendScreenTest : BitwardenComposeTest() {
viewModel.trySendAction(SendAction.UpgradedToPremiumCardDismiss)
}
}
@Test
fun `UpgradedToPremium action card should display in Empty viewState when eligible`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Empty,
isUpgradedToPremiumCardEligible = true,
)
}
composeTestRule
.onNodeWithText(text = "Upgraded to Premium")
.assertIsDisplayed()
composeTestRule
.onNodeWithText(text = "Learn more")
.assertIsDisplayed()
}
@Test
fun `UpgradedToPremium action card should not display in Loading viewState`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Loading,
isUpgradedToPremiumCardEligible = true,
)
}
composeTestRule
.onNodeWithText(text = "Upgraded to Premium")
.assertDoesNotExist()
}
@Test
fun `UpgradedToPremium action card should not display in Error viewState`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Error("Fail".asText()),
isUpgradedToPremiumCardEligible = true,
)
}
composeTestRule
.onNodeWithText(text = "Upgraded to Premium")
.assertDoesNotExist()
}
}
private val DEFAULT_STATE: SendState = SendState(