mirror of
https://github.com/bitwarden/android.git
synced 2026-10-22 02:53:19 -05:00
[PM-37181] feat: Surface Expired subscription substate (#6982)
This commit is contained in:
1 parent
8661dfaf2f
commit
230c8f769d
11 files changed
+255
-74
No files matched your search
+1
@@ -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,
|
||||
|
||||
+7
@@ -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.
|
||||
*/
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
+90
-64
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+23
-1
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+5
-3
@@ -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
|
||||
|
||||
+95
-2
@@ -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
|
||||
|
||||
+26
@@ -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 {
|
||||
|
||||
@@ -1303,19 +1303,21 @@ Do you want to switch to this account?</string>
|
||||
<string name="cancel_premium">Cancel Premium</string>
|
||||
<string name="subscription_status_active">Active</string>
|
||||
<string name="subscription_status_canceled">Canceled</string>
|
||||
<string name="subscription_status_expired">Expired</string>
|
||||
<string name="subscription_status_past_due">Past due</string>
|
||||
<string name="subscription_status_paused">Paused</string>
|
||||
<string name="subscription_status_update_payment">Update payment</string>
|
||||
<string name="subscription_status_pending_cancellation">Pending cancellation</string>
|
||||
<string name="subscription_pending_cancellation_description">Your subscription is scheduled to cancel on <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation>. You can reinstate it anytime before then.</string>
|
||||
<string name="premium_next_charge_summary">Your next charge is for <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation> USD due on <annotation emphasis="bold"><annotation arg="1">%2$s</annotation></annotation>.</string>
|
||||
<string name="subscription_canceled_description">Your subscription was canceled on <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation>. Resubscribe to continue using premium features.</string>
|
||||
<string name="subscription_canceled_description">Your subscription was canceled on <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation>. Resubscribe to continue using Premium features.</string>
|
||||
<string name="subscription_update_payment_description">We couldn’t process your payment. Update your payment before your subscription ends on <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation>.</string>
|
||||
<plurals name="subscription_past_due_description">
|
||||
<item quantity="one">You have a grace period of <annotation arg="0">%1$d</annotation> day from your subscription expiration date. Please resolve the past due amount by <annotation emphasis="bold"><annotation arg="1">%2$s</annotation></annotation>.</item>
|
||||
<item quantity="other">You have a grace period of <annotation arg="0">%1$d</annotation> days from your subscription expiration date. Please resolve the past due amount by <annotation emphasis="bold"><annotation arg="1">%2$s</annotation></annotation>.</item>
|
||||
</plurals>
|
||||
<string name="subscription_paused_description">Your subscription is paused. Resume to continue using premium features.</string>
|
||||
<string name="subscription_expired_description">Your subscription expired on <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation>. Resubscribe to continue using Premium features.</string>
|
||||
<string name="loading_subscription">Loading subscription…</string>
|
||||
<string name="loading_portal">Loading portal…</string>
|
||||
<string name="portal_error">Something went wrong</string>
|
||||
|
||||
Reference in new issue
Block a user