From a5f7288208d3bd4221f1a39fde79a2ea03841c8a Mon Sep 17 00:00:00 2001
From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Date: Fri, 22 May 2026 14:45:57 -0400
Subject: [PATCH] [PM-37916] chore: Align Premium subscription card line items
with Web (#6961)
---
.../feature/premium/plan/PlanScreen.kt | 112 ++++++++++++++----
.../feature/premium/plan/PlanViewModel.kt | 52 +++++---
.../feature/premium/plan/PlanScreenTest.kt | 90 ++++++++++++--
.../feature/premium/plan/PlanViewModelTest.kt | 86 ++++++++++++--
.../theme/type/BitwardenTypography.kt | 1 +
.../ui/platform/theme/type/Typography.kt | 12 ++
ui/src/main/res/values/strings.xml | 1 +
7 files changed, 296 insertions(+), 58 deletions(-)
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreen.kt
index c3edc100d5..1c2776c0c8 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreen.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreen.kt
@@ -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 = {},
+ ),
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModel.kt
index 69f037623e..74b63da686 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModel.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModel.kt
@@ -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,
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreenTest.kt
index 69784b71d4..62587bef1c 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreenTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreenTest.kt
@@ -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,
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModelTest.kt
index 24816c8e10..6aed400d83 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModelTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanViewModelTest.kt
@@ -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,
diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/theme/type/BitwardenTypography.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/theme/type/BitwardenTypography.kt
index 447f919881..8f05a20152 100644
--- a/ui/src/main/kotlin/com/bitwarden/ui/platform/theme/type/BitwardenTypography.kt
+++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/theme/type/BitwardenTypography.kt
@@ -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,
diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/theme/type/Typography.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/theme/type/Typography.kt
index 104d0c4592..f58855a4bf 100644
--- a/ui/src/main/kotlin/com/bitwarden/ui/platform/theme/type/Typography.kt
+++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/theme/type/Typography.kt
@@ -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,
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index 17b6dff0dd..787c71699a 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -1295,6 +1295,7 @@ Do you want to switch to this account?
Storage cost
Discount
Estimated tax
+ Total
%1$s / year
%1$s / month
Manage plan