mirror of
https://github.com/bitwarden/android.git
synced 2026-05-24 07:01:26 -05:00
[PM-37916] chore: Align Premium subscription card line items with Web (#6961)
This commit is contained in:
@@ -31,6 +31,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -607,28 +608,26 @@ private fun SubscriptionCard(
|
||||
modifier = rowModifier,
|
||||
)
|
||||
|
||||
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
|
||||
viewState.storageCostText?.let { storageCostText ->
|
||||
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
|
||||
SubscriptionLineItem(
|
||||
label = stringResource(id = BitwardenString.storage_cost),
|
||||
value = storageCostText,
|
||||
testTag = "StorageCostRow",
|
||||
modifier = rowModifier,
|
||||
)
|
||||
}
|
||||
|
||||
SubscriptionLineItem(
|
||||
label = stringResource(id = BitwardenString.storage_cost),
|
||||
value = viewState.storageCostText,
|
||||
testTag = "StorageCostRow",
|
||||
modifier = rowModifier,
|
||||
)
|
||||
|
||||
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
|
||||
|
||||
SubscriptionLineItem(
|
||||
label = stringResource(id = BitwardenString.discount),
|
||||
value = viewState.discountAmountText,
|
||||
valueColor = if (viewState.discountAmountText == "--") {
|
||||
BitwardenTheme.colorScheme.text.primary
|
||||
} else {
|
||||
BitwardenTheme.colorScheme.statusBadge.success.text
|
||||
},
|
||||
testTag = "DiscountRow",
|
||||
modifier = rowModifier,
|
||||
)
|
||||
viewState.discountAmountText?.let { discountAmountText ->
|
||||
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
|
||||
SubscriptionLineItem(
|
||||
label = stringResource(id = BitwardenString.discount),
|
||||
value = discountAmountText,
|
||||
testTag = "DiscountRow",
|
||||
modifier = rowModifier,
|
||||
valueColor = BitwardenTheme.colorScheme.statusBadge.success.text,
|
||||
)
|
||||
}
|
||||
|
||||
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
|
||||
|
||||
@@ -638,6 +637,16 @@ private fun SubscriptionCard(
|
||||
testTag = "EstimatedTaxRow",
|
||||
modifier = rowModifier,
|
||||
)
|
||||
|
||||
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
|
||||
|
||||
SubscriptionLineItem(
|
||||
label = stringResource(id = BitwardenString.total),
|
||||
value = viewState.totalText(),
|
||||
testTag = "TotalRow",
|
||||
modifier = rowModifier,
|
||||
labelStyle = BitwardenTheme.typography.bodyLargeEmphasis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,6 +713,10 @@ private fun subscriptionDescriptionText(
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
textStyle = BitwardenTheme.typography.bodyMedium,
|
||||
)
|
||||
val emphasisStyle = spanStyleOf(
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
textStyle = BitwardenTheme.typography.bodyMediumEmphasis,
|
||||
)
|
||||
return when (status) {
|
||||
PremiumSubscriptionStatus.ACTIVE -> annotatedStringResource(
|
||||
id = BitwardenString.premium_next_charge_summary,
|
||||
@@ -712,24 +725,28 @@ private fun subscriptionDescriptionText(
|
||||
nextChargeDateText ?: PLACEHOLDER_TEXT,
|
||||
),
|
||||
style = baseStyle,
|
||||
emphasisHighlightStyle = emphasisStyle,
|
||||
)
|
||||
|
||||
PremiumSubscriptionStatus.CANCELED -> annotatedStringResource(
|
||||
id = BitwardenString.subscription_canceled_description,
|
||||
args = arrayOf(canceledDateText ?: suspensionDateText ?: PLACEHOLDER_TEXT),
|
||||
style = baseStyle,
|
||||
emphasisHighlightStyle = emphasisStyle,
|
||||
)
|
||||
|
||||
PremiumSubscriptionStatus.PENDING_CANCELLATION -> annotatedStringResource(
|
||||
id = BitwardenString.subscription_pending_cancellation_description,
|
||||
args = arrayOf(cancelAtDateText ?: PLACEHOLDER_TEXT),
|
||||
style = baseStyle,
|
||||
emphasisHighlightStyle = emphasisStyle,
|
||||
)
|
||||
|
||||
PremiumSubscriptionStatus.UPDATE_PAYMENT -> annotatedStringResource(
|
||||
id = BitwardenString.subscription_update_payment_description,
|
||||
args = arrayOf(suspensionDateText ?: PLACEHOLDER_TEXT),
|
||||
style = baseStyle,
|
||||
emphasisHighlightStyle = emphasisStyle,
|
||||
)
|
||||
|
||||
PremiumSubscriptionStatus.PAST_DUE -> {
|
||||
@@ -740,6 +757,7 @@ private fun subscriptionDescriptionText(
|
||||
days.toString(),
|
||||
suspensionDateText ?: PLACEHOLDER_TEXT,
|
||||
style = baseStyle,
|
||||
emphasisHighlightStyle = emphasisStyle,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -757,6 +775,9 @@ private fun SubscriptionLineItem(
|
||||
value: String,
|
||||
testTag: String,
|
||||
modifier: Modifier = Modifier,
|
||||
labelStyle: TextStyle = BitwardenTheme.typography.bodyLarge,
|
||||
labelColor: Color = BitwardenTheme.colorScheme.text.secondary,
|
||||
valueStyle: TextStyle = BitwardenTheme.typography.bodyLarge,
|
||||
valueColor: Color = BitwardenTheme.colorScheme.text.primary,
|
||||
) {
|
||||
Row(
|
||||
@@ -768,12 +789,12 @@ private fun SubscriptionLineItem(
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
style = labelStyle,
|
||||
color = labelColor,
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
style = valueStyle,
|
||||
color = valueColor,
|
||||
)
|
||||
}
|
||||
@@ -839,6 +860,7 @@ private fun PlanScreenPremiumAccount_preview() {
|
||||
storageCostText = "$24.00",
|
||||
discountAmountText = "-$2.10",
|
||||
estimatedTaxText = "$3.85",
|
||||
totalText = BitwardenString.billing_rate_per_year.asText("$45.55"),
|
||||
nextChargeTotalText = "$45.55",
|
||||
nextChargeDateText = "April 2, 2026",
|
||||
showCancelButton = true,
|
||||
@@ -866,3 +888,45 @@ private fun PlanScreenPremiumAccount_preview() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@OmitFromCoverage
|
||||
@Composable
|
||||
private fun PlanScreenPremiumAccountZeroState_preview() {
|
||||
BitwardenTheme {
|
||||
BitwardenScaffold {
|
||||
PremiumContent(
|
||||
viewState = PlanState.ViewState.Premium(
|
||||
status = PremiumSubscriptionStatus.ACTIVE,
|
||||
billingAmountText = BitwardenString.billing_rate_per_year.asText("$19.80"),
|
||||
storageCostText = null,
|
||||
discountAmountText = null,
|
||||
estimatedTaxText = "$0.00",
|
||||
totalText = BitwardenString.billing_rate_per_year.asText("$19.80"),
|
||||
nextChargeTotalText = "$19.80",
|
||||
nextChargeDateText = "April 2, 2026",
|
||||
showCancelButton = true,
|
||||
),
|
||||
handlers = PlanHandlers(
|
||||
onBackClick = {},
|
||||
onUpgradeNowClick = {},
|
||||
onDismissError = {},
|
||||
onRetryClick = {},
|
||||
onRetryPricingClick = {},
|
||||
onClosePricingErrorClick = {},
|
||||
onCancelWaiting = {},
|
||||
onGoBackClick = {},
|
||||
onSyncClick = {},
|
||||
onContinueClick = {},
|
||||
onManagePlanClick = {},
|
||||
onCancelPremiumClick = {},
|
||||
onConfirmCancelClick = {},
|
||||
onDismissCancelConfirmation = {},
|
||||
onDismissPortalError = {},
|
||||
onRetryPortalClick = {},
|
||||
onRetrySubscriptionClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,9 +676,10 @@ class PlanViewModel @Inject constructor(
|
||||
return PlanState.ViewState.Premium(
|
||||
status = status,
|
||||
billingAmountText = seatsCost.toBillingAmountText(cadence),
|
||||
storageCostText = storageCost.toMoneyText(),
|
||||
discountAmountText = discountAmount.toMoneyText(negative = true),
|
||||
estimatedTaxText = estimatedTax.toMoneyText(),
|
||||
storageCostText = storageCost.toOptionalMoneyText(),
|
||||
discountAmountText = discountAmount.toOptionalMoneyText(negative = true),
|
||||
estimatedTaxText = estimatedTax.toRequiredMoneyText(),
|
||||
totalText = nextChargeTotal.toBillingAmountText(cadence),
|
||||
nextChargeTotalText = formattedTotal,
|
||||
nextChargeDateText = formattedDate,
|
||||
cancelAtDateText = formattedCancelAt,
|
||||
@@ -690,7 +691,6 @@ class PlanViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun BigDecimal.toBillingAmountText(cadence: PlanCadence): Text {
|
||||
if (this.signum() == 0) return PLACEHOLDER_TEXT.asText()
|
||||
val formatted = currencyFormatter.format(this)
|
||||
val cadenceRes = when (cadence) {
|
||||
PlanCadence.ANNUALLY -> BitwardenString.billing_rate_per_year
|
||||
@@ -699,9 +699,23 @@ class PlanViewModel @Inject constructor(
|
||||
return cadenceRes.asText(formatted)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.toMoneyText(negative: Boolean = false): String =
|
||||
/**
|
||||
* Formats this amount for an always-rendered line item. Null is coerced to zero so the row
|
||||
* still shows the locale-formatted `$0.00`, matching the Web convention of always rendering
|
||||
* the Estimated Tax and Total rows.
|
||||
*/
|
||||
private fun BigDecimal?.toRequiredMoneyText(): String =
|
||||
currencyFormatter.format(this ?: BigDecimal.ZERO)
|
||||
|
||||
/**
|
||||
* Formats this amount for a hide-when-absent line item. Returns `null` when the amount is
|
||||
* `null` or non-positive so the caller can omit the row entirely (Discount, Storage).
|
||||
* When [negative] is true, the formatted value is prefixed with `-` to match the canonical
|
||||
* Web discount styling.
|
||||
*/
|
||||
private fun BigDecimal?.toOptionalMoneyText(negative: Boolean = false): String? =
|
||||
when {
|
||||
this == null || this.signum() == 0 -> PLACEHOLDER_TEXT
|
||||
this == null || this.signum() <= 0 -> null
|
||||
negative -> "-${currencyFormatter.format(this)}"
|
||||
else -> currencyFormatter.format(this)
|
||||
}
|
||||
@@ -802,18 +816,28 @@ data class PlanState(
|
||||
/**
|
||||
* Premium user view — shows subscription details and management options.
|
||||
*
|
||||
* Line-item text fields are always populated: they default to the
|
||||
* `"--"` placeholder during the initial load and for any value that
|
||||
* resolves to null or `0.00` (e.g. no additional storage, no discount,
|
||||
* no tax).
|
||||
* Line-item text fields follow two visibility contracts that mirror the
|
||||
* canonical Web subscription card:
|
||||
*
|
||||
* - **Required** ([billingAmountText], [estimatedTaxText], [totalText]):
|
||||
* the row is always rendered. A zero amount is formatted as `$0.00`
|
||||
* rather than hidden. Defaults are sensible empty values used only
|
||||
* during the initial load — the `DialogState.Loading` overlay covers
|
||||
* the screen during the fetch, so these defaults are never surfaced
|
||||
* to the user.
|
||||
* - **Optional** ([storageCostText], [discountAmountText]): a `null`
|
||||
* value signals the screen to omit the row entirely (along with its
|
||||
* leading divider). When non-null, the value is fully formatted by
|
||||
* the view model — the screen renders it verbatim.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Premium(
|
||||
val status: PremiumSubscriptionStatus? = null,
|
||||
val billingAmountText: Text = PLACEHOLDER_TEXT.asText(),
|
||||
val storageCostText: String = PLACEHOLDER_TEXT,
|
||||
val discountAmountText: String = PLACEHOLDER_TEXT,
|
||||
val estimatedTaxText: String = PLACEHOLDER_TEXT,
|
||||
val billingAmountText: Text = "".asText(),
|
||||
val storageCostText: String? = null,
|
||||
val discountAmountText: String? = null,
|
||||
val estimatedTaxText: String = "$0.00",
|
||||
val totalText: Text = "".asText(),
|
||||
val nextChargeTotalText: String? = null,
|
||||
val nextChargeDateText: String? = null,
|
||||
val cancelAtDateText: String? = null,
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.semantics.getOrNull
|
||||
import androidx.compose.ui.test.SemanticsNodeInteraction
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
@@ -778,7 +777,7 @@ class PlanScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storage cost row should display storageCostText value`() {
|
||||
fun `storage cost row should display storageCostText value when populated`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PREMIUM_VIEW_STATE) }
|
||||
composeTestRule
|
||||
.onNodeWithTag("StorageCostRow")
|
||||
@@ -789,7 +788,17 @@ class PlanScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `discount row should display discountAmountText value`() {
|
||||
fun `storage cost row should not render when storageCostText is null`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = DEFAULT_PREMIUM_VIEW_STATE.copy(storageCostText = null))
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithTag("StorageCostRow")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `discount row should display discountAmountText value when populated`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PREMIUM_VIEW_STATE) }
|
||||
composeTestRule
|
||||
.onNodeWithTag("DiscountRow")
|
||||
@@ -800,7 +809,17 @@ class PlanScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `estimated tax row should display estimatedTaxText value`() {
|
||||
fun `discount row should not render when discountAmountText is null`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = DEFAULT_PREMIUM_VIEW_STATE.copy(discountAmountText = null))
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithTag("DiscountRow")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `estimated tax row should always display estimatedTaxText value`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PREMIUM_VIEW_STATE) }
|
||||
composeTestRule
|
||||
.onNodeWithTag("EstimatedTaxRow")
|
||||
@@ -811,14 +830,66 @@ class PlanScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `line items should display -- placeholder when values are defaults`() {
|
||||
fun `estimated tax row should display dollar zero zero when amount is zero`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = PlanState.ViewState.Premium())
|
||||
it.copy(viewState = DEFAULT_PREMIUM_VIEW_STATE.copy(estimatedTaxText = "$0.00"))
|
||||
}
|
||||
// Four rows, each displaying the default placeholder value "--".
|
||||
composeTestRule
|
||||
.onAllNodesWithText("--")
|
||||
.assertCountEquals(4)
|
||||
.onNodeWithTag("EstimatedTaxRow")
|
||||
.assertExists()
|
||||
composeTestRule
|
||||
.onNodeWithText("$0.00")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `total row should always display totalText value with cadence suffix`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PREMIUM_VIEW_STATE) }
|
||||
composeTestRule
|
||||
.onNodeWithTag("TotalRow")
|
||||
.assertExists()
|
||||
composeTestRule
|
||||
.onNodeWithText("$45.55 / year")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `total row should display monthly cadence suffix when cadence is monthly`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_PREMIUM_VIEW_STATE.copy(
|
||||
totalText = BitwardenString.billing_rate_per_month.asText("$0.00"),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithTag("TotalRow")
|
||||
.assertExists()
|
||||
composeTestRule
|
||||
.onNodeWithText("$0.00 / month")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `discount and storage rows should both hide when both texts are null`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_PREMIUM_VIEW_STATE.copy(
|
||||
storageCostText = null,
|
||||
discountAmountText = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithTag("StorageCostRow")
|
||||
.assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onNodeWithTag("DiscountRow")
|
||||
.assertDoesNotExist()
|
||||
// Billing, Tax, and Total are always rendered.
|
||||
composeTestRule.onNodeWithTag("BillingAmountRow").assertExists()
|
||||
composeTestRule.onNodeWithTag("EstimatedTaxRow").assertExists()
|
||||
composeTestRule.onNodeWithTag("TotalRow").assertExists()
|
||||
}
|
||||
|
||||
// endregion Line items
|
||||
@@ -1273,6 +1344,7 @@ private val DEFAULT_PREMIUM_VIEW_STATE = PlanState.ViewState.Premium(
|
||||
storageCostText = "$24.00",
|
||||
discountAmountText = "-$2.10",
|
||||
estimatedTaxText = "$3.85",
|
||||
totalText = BitwardenString.billing_rate_per_year.asText("$45.55"),
|
||||
nextChargeTotalText = "$45.55",
|
||||
nextChargeDateText = "April 2, 2026",
|
||||
showCancelButton = true,
|
||||
|
||||
@@ -1172,6 +1172,9 @@ class PlanViewModelTest : BaseViewModelTest() {
|
||||
billingAmountText = BitwardenString
|
||||
.billing_rate_per_month
|
||||
.asText("$19.80"),
|
||||
totalText = BitwardenString
|
||||
.billing_rate_per_month
|
||||
.asText("$45.55"),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
@@ -1180,7 +1183,7 @@ class PlanViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubscriptionResultReceive Success with zero seatsCost shows placeholder rate`() =
|
||||
fun `SubscriptionResultReceive Success with zero seatsCost still renders billing row`() =
|
||||
runTest {
|
||||
markUserPremium()
|
||||
|
||||
@@ -1196,7 +1199,9 @@ class PlanViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(
|
||||
DEFAULT_PREMIUM_LOADED_STATE.copy(
|
||||
viewState = DEFAULT_PREMIUM_ACTIVE_VIEW_STATE.copy(
|
||||
billingAmountText = PLACEHOLDER.asText(),
|
||||
billingAmountText = BitwardenString
|
||||
.billing_rate_per_year
|
||||
.asText("$0.00"),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
@@ -1205,7 +1210,7 @@ class PlanViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubscriptionResultReceive Success with null line items shows placeholder text`() =
|
||||
fun `SubscriptionResultReceive Success with null line items hides discount and storage rows`() =
|
||||
runTest {
|
||||
markUserPremium()
|
||||
|
||||
@@ -1222,8 +1227,8 @@ class PlanViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(
|
||||
DEFAULT_PREMIUM_LOADED_STATE.copy(
|
||||
viewState = DEFAULT_PREMIUM_ACTIVE_VIEW_STATE.copy(
|
||||
storageCostText = PLACEHOLDER,
|
||||
discountAmountText = PLACEHOLDER,
|
||||
storageCostText = null,
|
||||
discountAmountText = null,
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
@@ -1232,7 +1237,7 @@ class PlanViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubscriptionResultReceive Success with zero line items shows placeholder text`() =
|
||||
fun `SubscriptionResultReceive Success with zero line items hides discount and storage rows`() =
|
||||
runTest {
|
||||
markUserPremium()
|
||||
|
||||
@@ -1250,9 +1255,69 @@ class PlanViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(
|
||||
DEFAULT_PREMIUM_LOADED_STATE.copy(
|
||||
viewState = DEFAULT_PREMIUM_ACTIVE_VIEW_STATE.copy(
|
||||
storageCostText = PLACEHOLDER,
|
||||
discountAmountText = PLACEHOLDER,
|
||||
estimatedTaxText = PLACEHOLDER,
|
||||
storageCostText = null,
|
||||
discountAmountText = null,
|
||||
estimatedTaxText = "$0.00",
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubscriptionResultReceive Success with zero nextChargeTotal renders zero total row`() =
|
||||
runTest {
|
||||
markUserPremium()
|
||||
|
||||
val viewModel = createViewModel(
|
||||
subscriptionResult = SubscriptionResult.Success(
|
||||
subscription = SUBSCRIPTION_INFO_ACTIVE.copy(
|
||||
nextChargeTotal = BigDecimal.ZERO,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_PREMIUM_LOADED_STATE.copy(
|
||||
viewState = DEFAULT_PREMIUM_ACTIVE_VIEW_STATE.copy(
|
||||
totalText = BitwardenString
|
||||
.billing_rate_per_year
|
||||
.asText("$0.00"),
|
||||
nextChargeTotalText = "$0.00",
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubscriptionResultReceive Success with Monthly cadence renders total with month suffix`() =
|
||||
runTest {
|
||||
markUserPremium()
|
||||
|
||||
val viewModel = createViewModel(
|
||||
subscriptionResult = SubscriptionResult.Success(
|
||||
subscription = SUBSCRIPTION_INFO_ACTIVE.copy(
|
||||
cadence = PlanCadence.MONTHLY,
|
||||
nextChargeTotal = BigDecimal.ZERO,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_PREMIUM_LOADED_STATE.copy(
|
||||
viewState = DEFAULT_PREMIUM_ACTIVE_VIEW_STATE.copy(
|
||||
billingAmountText = BitwardenString
|
||||
.billing_rate_per_month
|
||||
.asText("$19.80"),
|
||||
totalText = BitwardenString
|
||||
.billing_rate_per_month
|
||||
.asText("$0.00"),
|
||||
nextChargeTotalText = "$0.00",
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
@@ -1738,14 +1803,13 @@ private val DEFAULT_PRICING_SUCCESS = PremiumPlanPricingResult.Success(
|
||||
annualPrice = ANNUAL_PRICE,
|
||||
)
|
||||
|
||||
private const val PLACEHOLDER = "--"
|
||||
|
||||
private val DEFAULT_PREMIUM_ACTIVE_VIEW_STATE = PlanState.ViewState.Premium(
|
||||
status = PremiumSubscriptionStatus.ACTIVE,
|
||||
billingAmountText = BitwardenString.billing_rate_per_year.asText("$19.80"),
|
||||
storageCostText = "$24.00",
|
||||
discountAmountText = "-$2.10",
|
||||
estimatedTaxText = "$3.85",
|
||||
totalText = BitwardenString.billing_rate_per_year.asText("$45.55"),
|
||||
nextChargeTotalText = "$45.55",
|
||||
nextChargeDateText = "April 2, 2026",
|
||||
showCancelButton = true,
|
||||
|
||||
@@ -18,6 +18,7 @@ data class BitwardenTypography(
|
||||
val titleMedium: TextStyle,
|
||||
val titleSmall: TextStyle,
|
||||
val bodyLarge: TextStyle,
|
||||
val bodyLargeEmphasis: TextStyle,
|
||||
val bodyMedium: TextStyle,
|
||||
val bodyMediumEmphasis: TextStyle,
|
||||
val bodySmall: TextStyle,
|
||||
|
||||
@@ -145,6 +145,18 @@ val bitwardenTypography: BitwardenTypography = BitwardenTypography(
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
bodyLargeEmphasis = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
lineHeight = 20.sp,
|
||||
fontFamily = FontFamily(Font(R.font.dm_sans_regular)),
|
||||
fontWeight = FontWeight.W700,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 18.sp,
|
||||
|
||||
@@ -1295,6 +1295,7 @@ Do you want to switch to this account?</string>
|
||||
<string name="storage_cost">Storage cost</string>
|
||||
<string name="discount">Discount</string>
|
||||
<string name="estimated_tax">Estimated tax</string>
|
||||
<string name="total">Total</string>
|
||||
<string name="billing_rate_per_year">%1$s / year</string>
|
||||
<string name="billing_rate_per_month">%1$s / month</string>
|
||||
<string name="manage_plan">Manage plan</string>
|
||||
|
||||
Reference in New Issue
Block a user