diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerImpl.kt index 9dee16494a..f49a40679c 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerImpl.kt @@ -404,6 +404,7 @@ private fun SubscriptionStatusState.isInTroubleState(): Boolean = this is SubscriptionStatusState.Available && when (this.status) { PremiumSubscriptionStatus.CANCELED, + PremiumSubscriptionStatus.EXPIRED, PremiumSubscriptionStatus.PAST_DUE, PremiumSubscriptionStatus.PAUSED, PremiumSubscriptionStatus.UPDATE_PAYMENT, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/repository/model/PremiumSubscriptionStatus.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/repository/model/PremiumSubscriptionStatus.kt index 4891ba56c0..98794a9a16 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/repository/model/PremiumSubscriptionStatus.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/repository/model/PremiumSubscriptionStatus.kt @@ -7,6 +7,13 @@ enum class PremiumSubscriptionStatus { ACTIVE, CANCELED, + /** + * The subscription's initial payment never succeeded and Stripe voided the invoice, so + * the subscription never became active. Distinct from [CANCELED], which describes a + * subscription that was previously active. + */ + EXPIRED, + /** * The subscription is scheduled to cancel at a future date but is still active until then. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/repository/util/BitwardenSubscriptionResponseJsonExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/repository/util/BitwardenSubscriptionResponseJsonExtensions.kt index 769c5f356a..05ec7952e5 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/repository/util/BitwardenSubscriptionResponseJsonExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/billing/repository/util/BitwardenSubscriptionResponseJsonExtensions.kt @@ -64,10 +64,9 @@ private fun BitwardenSubscriptionResponseJson.toPremiumSubscriptionStatus(): } } - SubscriptionStatusJson.CANCELED, - SubscriptionStatusJson.INCOMPLETE_EXPIRED, - -> PremiumSubscriptionStatus.CANCELED + SubscriptionStatusJson.CANCELED -> PremiumSubscriptionStatus.CANCELED + SubscriptionStatusJson.INCOMPLETE_EXPIRED -> PremiumSubscriptionStatus.EXPIRED SubscriptionStatusJson.INCOMPLETE, SubscriptionStatusJson.UNPAID, -> PremiumSubscriptionStatus.UPDATE_PAYMENT 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 a0bdede28f..616c80f9d8 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 @@ -4,6 +4,7 @@ package com.x8bit.bitwarden.ui.platform.feature.premium.plan import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -72,6 +73,7 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalAuthTabLaunchers import com.x8bit.bitwarden.ui.platform.feature.premium.plan.handlers.PlanHandlers import com.x8bit.bitwarden.ui.platform.feature.premium.plan.util.badgeColors import com.x8bit.bitwarden.ui.platform.feature.premium.plan.util.labelRes +import com.x8bit.bitwarden.ui.platform.feature.premium.plan.util.showsFeatureList import com.x8bit.bitwarden.ui.platform.model.AuthTabLaunchers private const val PLACEHOLDER_TEXT: String = "--" @@ -393,25 +395,30 @@ private fun PremiumFeaturesCard( .standardHorizontalMargin(), ) - BitwardenHorizontalDivider() + PremiumFeatureRows() + } +} - val features = listOf( - BitwardenString.built_in_authenticator, - BitwardenString.emergency_access, - BitwardenString.secure_file_storage, - BitwardenString.breach_monitoring, +@Composable +private fun ColumnScope.PremiumFeatureRows() { + BitwardenHorizontalDivider() + + val features = listOf( + BitwardenString.built_in_authenticator, + BitwardenString.emergency_access, + BitwardenString.secure_file_storage, + BitwardenString.breach_monitoring, + ) + features.forEachIndexed { index, featureStringRes -> + BitwardenContentBlock( + data = ContentBlockData( + headerText = stringResource(id = featureStringRes), + iconVectorResource = BitwardenDrawable.ic_check_mark, + ), + headerTextStyle = BitwardenTheme.typography.titleMedium, + showDivider = index != features.lastIndex, + modifier = Modifier.padding(vertical = 8.dp), ) - features.forEachIndexed { index, featureStringRes -> - BitwardenContentBlock( - data = ContentBlockData( - headerText = stringResource(id = featureStringRes), - iconVectorResource = BitwardenDrawable.ic_check_mark, - ), - headerTextStyle = BitwardenTheme.typography.titleMedium, - showDivider = index != features.lastIndex, - modifier = Modifier.padding(vertical = 8.dp), - ) - } } } @@ -562,21 +569,18 @@ private fun PremiumContent( } } -@Suppress("LongMethod") @Composable private fun SubscriptionCard( viewState: PlanState.ViewState.Premium, modifier: Modifier = Modifier, ) { - val rowModifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() Column( modifier = modifier .fillMaxWidth() .cardStyle( cardStyle = CardStyle.Full, - // Override bottom padding; the final row owns its own spacing. + // Override bottom padding; the final row (line item or feature) owns its + // own spacing. paddingBottom = 0.dp, ), ) { @@ -593,55 +597,70 @@ private fun SubscriptionCard( .standardHorizontalMargin(), ) - BitwardenHorizontalDivider() - - SubscriptionLineItem( - label = stringResource(id = BitwardenString.billing_amount), - value = viewState.billingAmountText(), - testTag = "BillingAmountRow", - modifier = rowModifier, - ) - - viewState.storageCostText?.let { storageCostText -> - BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) - SubscriptionLineItem( - label = stringResource(id = BitwardenString.storage_cost), - value = storageCostText, - testTag = "StorageCostRow", - modifier = rowModifier, - ) + if (viewState.status?.showsFeatureList() == true) { + PremiumFeatureRows() + } else { + SubscriptionLineItems(viewState = viewState) } + } +} - 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, - ) - } +@Composable +private fun SubscriptionLineItems( + viewState: PlanState.ViewState.Premium, +) { + val rowModifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + BitwardenHorizontalDivider() + + SubscriptionLineItem( + label = stringResource(id = BitwardenString.billing_amount), + value = viewState.billingAmountText(), + testTag = "BillingAmountRow", + modifier = rowModifier, + ) + + viewState.storageCostText?.let { storageCostText -> BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) - SubscriptionLineItem( - label = stringResource(id = BitwardenString.estimated_tax), - value = viewState.estimatedTaxText, - testTag = "EstimatedTaxRow", + label = stringResource(id = BitwardenString.storage_cost), + value = storageCostText, + testTag = "StorageCostRow", 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, - ) } + + 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)) + + SubscriptionLineItem( + label = stringResource(id = BitwardenString.estimated_tax), + value = viewState.estimatedTaxText, + 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, + ) } @Composable @@ -692,7 +711,7 @@ private fun SubscriptionHeader( } } -@Suppress("CyclomaticComplexMethod") +@Suppress("CyclomaticComplexMethod", "LongMethod") @Composable private fun subscriptionDescriptionText( status: PremiumSubscriptionStatus?, @@ -759,6 +778,13 @@ private fun subscriptionDescriptionText( stringResource(id = BitwardenString.subscription_paused_description), ) + PremiumSubscriptionStatus.EXPIRED -> annotatedStringResource( + id = BitwardenString.subscription_expired_description, + args = arrayOf(suspensionDateText ?: PLACEHOLDER_TEXT), + style = baseStyle, + emphasisHighlightStyle = emphasisStyle, + ) + null -> null } } 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 7c505027ce..e64ca73890 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 @@ -1175,6 +1175,7 @@ sealed class PlanAction { */ private fun PremiumSubscriptionStatus.canBeCanceled(): Boolean = when (this) { PremiumSubscriptionStatus.CANCELED, + PremiumSubscriptionStatus.EXPIRED, PremiumSubscriptionStatus.PENDING_CANCELLATION, -> false @@ -1193,6 +1194,7 @@ private fun PremiumSubscriptionStatus.canBeCanceled(): Boolean = when (this) { */ private fun PremiumSubscriptionStatus.isPremiumViewEligible(): Boolean = when (this) { PremiumSubscriptionStatus.CANCELED, + PremiumSubscriptionStatus.EXPIRED, PremiumSubscriptionStatus.PAST_DUE, PremiumSubscriptionStatus.PAUSED, PremiumSubscriptionStatus.PENDING_CANCELLATION, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/util/PremiumSubscriptionStatusExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/util/PremiumSubscriptionStatusExtensions.kt index 82731a71de..38f4ac5944 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/util/PremiumSubscriptionStatusExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/util/PremiumSubscriptionStatusExtensions.kt @@ -14,6 +14,7 @@ import com.x8bit.bitwarden.data.billing.repository.model.PremiumSubscriptionStat fun PremiumSubscriptionStatus.labelRes(): Int = when (this) { PremiumSubscriptionStatus.ACTIVE -> BitwardenString.subscription_status_active PremiumSubscriptionStatus.CANCELED -> BitwardenString.subscription_status_canceled + PremiumSubscriptionStatus.EXPIRED -> BitwardenString.subscription_status_expired PremiumSubscriptionStatus.PENDING_CANCELLATION -> { BitwardenString.subscription_status_pending_cancellation } @@ -23,6 +24,24 @@ fun PremiumSubscriptionStatus.labelRes(): Int = when (this) { PremiumSubscriptionStatus.UPDATE_PAYMENT -> BitwardenString.subscription_status_update_payment } +/** + * Returns `true` when the Premium plan card should replace its billing line items with the + * premium feature list. Reserved for terminal states where line items carry no actionable + * information and the user's path forward is to resubscribe. + */ +fun PremiumSubscriptionStatus.showsFeatureList(): Boolean = when (this) { + PremiumSubscriptionStatus.CANCELED, + PremiumSubscriptionStatus.EXPIRED, + -> true + + PremiumSubscriptionStatus.ACTIVE, + PremiumSubscriptionStatus.PAST_DUE, + PremiumSubscriptionStatus.PAUSED, + PremiumSubscriptionStatus.PENDING_CANCELLATION, + PremiumSubscriptionStatus.UPDATE_PAYMENT, + -> false +} + /** * Returns the [BitwardenColorScheme.StatusBadgeVariantColors] used to render the badge for a * [PremiumSubscriptionStatus]. @@ -31,7 +50,10 @@ fun PremiumSubscriptionStatus.labelRes(): Int = when (this) { fun PremiumSubscriptionStatus.badgeColors(): BitwardenColorScheme.StatusBadgeVariantColors = when (this) { PremiumSubscriptionStatus.ACTIVE -> BitwardenTheme.colorScheme.statusBadge.success - PremiumSubscriptionStatus.CANCELED -> BitwardenTheme.colorScheme.statusBadge.error + PremiumSubscriptionStatus.CANCELED, + PremiumSubscriptionStatus.EXPIRED, + -> BitwardenTheme.colorScheme.statusBadge.error + PremiumSubscriptionStatus.PAST_DUE, PremiumSubscriptionStatus.PAUSED, PremiumSubscriptionStatus.PENDING_CANCELLATION, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerTest.kt index 271b88163e..d9f57b0ad4 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/manager/PremiumStateManagerTest.kt @@ -1035,6 +1035,7 @@ class PremiumStateManagerTest { fun `banner eligible when account is premium but status is in a trouble state`() = runTest { listOf( PremiumSubscriptionStatus.CANCELED, + PremiumSubscriptionStatus.EXPIRED, PremiumSubscriptionStatus.PAST_DUE, PremiumSubscriptionStatus.PAUSED, PremiumSubscriptionStatus.UPDATE_PAYMENT, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/repository/util/BitwardenSubscriptionResponseJsonExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/repository/util/BitwardenSubscriptionResponseJsonExtensionsTest.kt index 7d25602a2a..2e0f38875e 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/repository/util/BitwardenSubscriptionResponseJsonExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/billing/repository/util/BitwardenSubscriptionResponseJsonExtensionsTest.kt @@ -29,16 +29,18 @@ class BitwardenSubscriptionResponseJsonExtensionsTest { @Test fun `toSubscriptionInfo maps CANCELED to CANCELED`() { - val info = buildResponse(status = SubscriptionStatusJson.CANCELED).toSubscriptionInfo() + val info = buildResponse( + status = SubscriptionStatusJson.CANCELED, + ).toSubscriptionInfo() assertEquals(PremiumSubscriptionStatus.CANCELED, info.status) } @Test - fun `toSubscriptionInfo maps INCOMPLETE_EXPIRED to CANCELED`() { + fun `toSubscriptionInfo maps INCOMPLETE_EXPIRED to EXPIRED`() { val info = buildResponse( status = SubscriptionStatusJson.INCOMPLETE_EXPIRED, ).toSubscriptionInfo() - assertEquals(PremiumSubscriptionStatus.CANCELED, info.status) + assertEquals(PremiumSubscriptionStatus.EXPIRED, info.status) } @Test 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 0f17108f45..22f2eb9dc3 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 @@ -503,6 +503,20 @@ class PlanScreenTest : BitwardenComposeTest() { .assertIsDisplayed() } + @Test + fun `status badge should render with Expired label for EXPIRED status`() { + mutableStateFlow.update { + it.copy( + viewState = DEFAULT_PREMIUM_VIEW_STATE.copy( + status = PremiumSubscriptionStatus.EXPIRED, + ), + ) + } + composeTestRule + .onNodeWithText("Expired") + .assertIsDisplayed() + } + @Test fun `status badge should render with Update payment label for UPDATE_PAYMENT status`() { mutableStateFlow.update { @@ -568,6 +582,7 @@ class PlanScreenTest : BitwardenComposeTest() { } composeTestRule.onNodeWithText("Active").assertDoesNotExist() composeTestRule.onNodeWithText("Canceled").assertDoesNotExist() + composeTestRule.onNodeWithText("Expired").assertDoesNotExist() composeTestRule.onNodeWithText("Past due").assertDoesNotExist() composeTestRule.onNodeWithText("Paused").assertDoesNotExist() composeTestRule.onNodeWithText("Pending cancellation").assertDoesNotExist() @@ -614,7 +629,7 @@ class PlanScreenTest : BitwardenComposeTest() { composeTestRule .onNodeWithText( "Your subscription was canceled on April 21, 2026. " + - "Resubscribe to continue using premium features.", + "Resubscribe to continue using Premium features.", ) .assertTextRangeHasBoldSpan(boldSubstring = "April 21, 2026") } @@ -633,7 +648,7 @@ class PlanScreenTest : BitwardenComposeTest() { composeTestRule .onNodeWithText( "Your subscription was canceled on May 15, 2026. " + - "Resubscribe to continue using premium features.", + "Resubscribe to continue using Premium features.", ) .assertTextRangeHasBoldSpan(boldSubstring = "May 15, 2026") } @@ -709,6 +724,24 @@ class PlanScreenTest : BitwardenComposeTest() { .assertTextRangeHasBoldSpan(boldSubstring = "May 1, 2026") } + @Test + fun `EXPIRED description should bold the suspension date`() { + mutableStateFlow.update { + it.copy( + viewState = DEFAULT_PREMIUM_VIEW_STATE.copy( + status = PremiumSubscriptionStatus.EXPIRED, + suspensionDateText = "April 21, 2026", + ), + ) + } + composeTestRule + .onNodeWithText( + "Your subscription expired on April 21, 2026. " + + "Resubscribe to continue using Premium features.", + ) + .assertTextRangeHasBoldSpan(boldSubstring = "April 21, 2026") + } + // endregion Premium content rendering // region Line items @@ -842,6 +875,66 @@ class PlanScreenTest : BitwardenComposeTest() { // endregion Line items + // region Terminal-state feature list + + @Test + fun `CANCELED status should render feature list instead of line items`() { + mutableStateFlow.update { + it.copy( + viewState = DEFAULT_PREMIUM_VIEW_STATE.copy( + status = PremiumSubscriptionStatus.CANCELED, + ), + ) + } + composeTestRule.onNodeWithText("Built-in authenticator").assertIsDisplayed() + composeTestRule.onNodeWithText("Emergency access").assertIsDisplayed() + composeTestRule.onNodeWithText("Secure file storage").assertIsDisplayed() + composeTestRule.onNodeWithText("Breach monitoring").assertIsDisplayed() + composeTestRule.onNodeWithTag("BillingAmountRow").assertDoesNotExist() + composeTestRule.onNodeWithTag("StorageCostRow").assertDoesNotExist() + composeTestRule.onNodeWithTag("DiscountRow").assertDoesNotExist() + composeTestRule.onNodeWithTag("EstimatedTaxRow").assertDoesNotExist() + composeTestRule.onNodeWithTag("TotalRow").assertDoesNotExist() + } + + @Test + fun `EXPIRED status should render feature list instead of line items`() { + mutableStateFlow.update { + it.copy( + viewState = DEFAULT_PREMIUM_VIEW_STATE.copy( + status = PremiumSubscriptionStatus.EXPIRED, + ), + ) + } + composeTestRule.onNodeWithText("Built-in authenticator").assertIsDisplayed() + composeTestRule.onNodeWithText("Emergency access").assertIsDisplayed() + composeTestRule.onNodeWithText("Secure file storage").assertIsDisplayed() + composeTestRule.onNodeWithText("Breach monitoring").assertIsDisplayed() + composeTestRule.onNodeWithTag("BillingAmountRow").assertDoesNotExist() + composeTestRule.onNodeWithTag("StorageCostRow").assertDoesNotExist() + composeTestRule.onNodeWithTag("DiscountRow").assertDoesNotExist() + composeTestRule.onNodeWithTag("EstimatedTaxRow").assertDoesNotExist() + composeTestRule.onNodeWithTag("TotalRow").assertDoesNotExist() + } + + @Test + fun `UPDATE_PAYMENT status should keep line items and not render feature list`() { + mutableStateFlow.update { + it.copy( + viewState = DEFAULT_PREMIUM_VIEW_STATE.copy( + status = PremiumSubscriptionStatus.UPDATE_PAYMENT, + ), + ) + } + composeTestRule.onNodeWithTag("BillingAmountRow").assertExists() + composeTestRule.onNodeWithTag("EstimatedTaxRow").assertExists() + composeTestRule.onNodeWithTag("TotalRow").assertExists() + composeTestRule.onNodeWithText("Built-in authenticator").assertDoesNotExist() + composeTestRule.onNodeWithText("Breach monitoring").assertDoesNotExist() + } + + // endregion Terminal-state feature list + // region Action buttons @Test 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 3fbc1ca54f..27187677d4 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 @@ -1221,6 +1221,32 @@ class PlanViewModelTest : BaseViewModelTest() { } } + @Test + fun `SubscriptionResultReceive Success with Expired status should hide cancel button`() = + runTest { + markUserPremium() + + val viewModel = createViewModel( + subscriptionResult = SubscriptionResult.Success( + subscription = SUBSCRIPTION_INFO_ACTIVE.copy( + status = PremiumSubscriptionStatus.EXPIRED, + ), + ), + ) + + viewModel.stateFlow.test { + assertEquals( + DEFAULT_PREMIUM_LOADED_STATE.copy( + viewState = DEFAULT_PREMIUM_ACTIVE_VIEW_STATE.copy( + status = PremiumSubscriptionStatus.EXPIRED, + showCancelButton = false, + ), + ), + awaitItem(), + ) + } + } + @Test fun `SubscriptionResultReceive Success with Monthly cadence formats per-month rate`() = runTest { diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 787c71699a..19f8d87b85 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1303,19 +1303,21 @@ Do you want to switch to this account? Cancel Premium Active Canceled + Expired Past due Paused Update payment Pending cancellation Your subscription is scheduled to cancel on %1$s. You can reinstate it anytime before then. Your next charge is for %1$s USD due on %2$s. - Your subscription was canceled on %1$s. Resubscribe to continue using premium features. + Your subscription was canceled on %1$s. Resubscribe to continue using Premium features. We couldn’t process your payment. Update your payment before your subscription ends on %1$s. You have a grace period of %1$d day from your subscription expiration date. Please resolve the past due amount by %2$s. You have a grace period of %1$d days from your subscription expiration date. Please resolve the past due amount by %2$s. Your subscription is paused. Resume to continue using premium features. + Your subscription expired on %1$s. Resubscribe to continue using Premium features. Loading subscription… Loading portal… Something went wrong