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