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 616c80f9d89..f712d13a20f 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 @@ -50,8 +50,11 @@ import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.bitwarden.ui.platform.components.badge.BitwardenStatusBadge import com.bitwarden.ui.platform.components.button.BitwardenFilledButton import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton +import com.bitwarden.ui.platform.components.button.model.BitwardenButtonData import com.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard import com.bitwarden.ui.platform.components.content.BitwardenContentBlock +import com.bitwarden.ui.platform.components.content.BitwardenErrorContent +import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent import com.bitwarden.ui.platform.components.content.model.ContentBlockData import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog @@ -70,6 +73,8 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.billing.repository.model.PremiumSubscriptionStatus import com.x8bit.bitwarden.ui.platform.composition.LocalAuthTabLaunchers +import com.x8bit.bitwarden.ui.platform.feature.premium.plan.PlanState.ViewState.Error.Type.PRICING_UNAVAILABLE +import com.x8bit.bitwarden.ui.platform.feature.premium.plan.PlanState.ViewState.Error.Type.SUBSCRIPTION 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 @@ -144,23 +149,47 @@ fun PlanScreen( }, ) { when (val viewState = state.viewState) { - is PlanState.ViewState.Free.Cloud -> { + is PlanState.ViewState.Content.Free.Cloud -> { FreeCloudContent( viewState = viewState, handlers = handlers, ) } - is PlanState.ViewState.Free.SelfHosted -> { + is PlanState.ViewState.Content.Free.SelfHosted -> { FreeSelfHostedContent() } - is PlanState.ViewState.Premium -> { + is PlanState.ViewState.Content.Premium -> { PremiumContent( viewState = viewState, handlers = handlers, ) } + + is PlanState.ViewState.Error -> { + BitwardenErrorContent( + illustrationData = IconData.Local(iconRes = BitwardenDrawable.ill_file_error), + message = viewState.message(), + buttonData = BitwardenButtonData( + label = BitwardenString.try_again.asText(), + onClick = { + when (viewState.type) { + PRICING_UNAVAILABLE -> handlers.onRetryPricingClick() + SUBSCRIPTION -> handlers.onRetrySubscriptionClick() + } + }, + ), + modifier = Modifier.fillMaxSize(), + ) + } + + is PlanState.ViewState.Loading -> { + BitwardenLoadingContent( + text = viewState.message(), + modifier = Modifier.fillMaxSize(), + ) + } } } } @@ -184,18 +213,6 @@ private fun PlanDialogs( ) } - is PlanState.DialogState.GetPricingError -> { - BitwardenTwoButtonDialog( - title = dialogState.title(), - message = dialogState.message(), - confirmButtonText = stringResource(BitwardenString.try_again), - dismissButtonText = stringResource(BitwardenString.close), - onConfirmClick = handlers.onRetryPricingClick, - onDismissClick = handlers.onClosePricingErrorClick, - onDismissRequest = handlers.onClosePricingErrorClick, - ) - } - is PlanState.DialogState.WaitingForPayment -> { BitwardenTwoButtonDialog( title = stringResource(id = BitwardenString.payment_not_received_yet), @@ -250,18 +267,6 @@ private fun PlanDialogs( ) } - is PlanState.DialogState.SubscriptionError -> { - BitwardenTwoButtonDialog( - title = dialogState.title(), - message = dialogState.message(), - confirmButtonText = stringResource(id = BitwardenString.try_again), - dismissButtonText = stringResource(id = BitwardenString.close), - onConfirmClick = handlers.onRetrySubscriptionClick, - onDismissClick = handlers.onBackClick, - onDismissRequest = handlers.onBackClick, - ) - } - PlanState.DialogState.LoadingPortal -> { BitwardenLoadingDialog( text = stringResource(id = BitwardenString.loading_portal), @@ -278,7 +283,7 @@ private fun PlanDialogs( @Composable private fun FreeCloudContent( - viewState: PlanState.ViewState.Free.Cloud, + viewState: PlanState.ViewState.Content.Free.Cloud, handlers: PlanHandlers, modifier: Modifier = Modifier, ) { @@ -504,7 +509,7 @@ private fun PriceRow( @Composable private fun PremiumContent( - viewState: PlanState.ViewState.Premium, + viewState: PlanState.ViewState.Content.Premium, handlers: PlanHandlers, modifier: Modifier = Modifier, ) { @@ -571,7 +576,7 @@ private fun PremiumContent( @Composable private fun SubscriptionCard( - viewState: PlanState.ViewState.Premium, + viewState: PlanState.ViewState.Content.Premium, modifier: Modifier = Modifier, ) { Column( @@ -607,7 +612,7 @@ private fun SubscriptionCard( @Composable private fun SubscriptionLineItems( - viewState: PlanState.ViewState.Premium, + viewState: PlanState.ViewState.Content.Premium, ) { val rowModifier = Modifier .fillMaxWidth() @@ -827,7 +832,7 @@ private fun PlanScreenFreeCloudAccount_preview() { BitwardenTheme { BitwardenScaffold { FreeCloudContent( - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = null, isAwaitingPremiumStatus = false, @@ -875,7 +880,7 @@ private fun PlanScreenPremiumAccount_preview() { BitwardenTheme { BitwardenScaffold { PremiumContent( - viewState = PlanState.ViewState.Premium( + viewState = PlanState.ViewState.Content.Premium( status = PremiumSubscriptionStatus.ACTIVE, billingAmountText = BitwardenString.billing_rate_per_year.asText("$19.80"), storageCostText = "$24.00", @@ -917,7 +922,7 @@ private fun PlanScreenPremiumAccountZeroState_preview() { BitwardenTheme { BitwardenScaffold { PremiumContent( - viewState = PlanState.ViewState.Premium( + viewState = PlanState.ViewState.Content.Premium( status = PremiumSubscriptionStatus.ACTIVE, billingAmountText = BitwardenString.billing_rate_per_year.asText("$19.80"), storageCostText = 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 33588aaeef5..12490196fe2 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 @@ -36,8 +36,10 @@ import com.x8bit.bitwarden.ui.platform.feature.premium.plan.util.toDiscountMoney import com.x8bit.bitwarden.ui.platform.feature.premium.plan.util.toPresentMoneyText import com.x8bit.bitwarden.ui.platform.feature.premium.plan.util.toRequiredMoneyText import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -86,80 +88,85 @@ class PlanViewModel @Inject constructor( premiumStateManager.subscriptionStatusStateFlow.value.isPremiumViewEligible() val isSelfHosted = premiumStateManager.isSelfHosted PlanState( + isSelfHosted = isSelfHosted, + showsPremiumView = showsPremiumView, planMode = planMode, viewState = when { - showsPremiumView -> PlanState.ViewState.Premium() - isSelfHosted -> PlanState.ViewState.Free.SelfHosted - else -> PlanState.ViewState.Free.Cloud( - rate = PLACEHOLDER_TEXT, - checkoutUrl = null, - isAwaitingPremiumStatus = false, - isPremiumUpgradePending = premiumStateManager - .upgradeLifecycleStateFlow - .value is UpgradeLifecycleState.UpgradePending, - ) + showsPremiumView -> { + // We are loading the premium data. + PlanState.ViewState.Loading( + message = BitwardenString.loading_subscription.asText(), + ) + } + + isSelfHosted -> { + // Nothing to load, we are good to go. + PlanState.ViewState.Content.Free.SelfHosted + } + + else -> { + // We are loading the plan details. + PlanState.ViewState.Loading( + message = BitwardenString.loading.asText(), + ) + } }, dialogState = null, ) }, ) { - private val currencyFormatter: NumberFormat = - NumberFormat.getCurrencyInstance(Locale.US) + private val currencyFormatter: NumberFormat = NumberFormat.getCurrencyInstance(Locale.US) init { stateFlow .onEach { savedStateHandle[KEY_STATE] = it } .launchIn(viewModelScope) - authRepository - .userStateFlow - .map { PlanAction.Internal.UserStateUpdateReceive(it) } - .onEach(::sendAction) - .launchIn(viewModelScope) - - specialCircumstanceManager - .specialCircumstanceStateFlow - .map { PlanAction.Internal.SpecialCircumstanceReceive(it) } - .onEach(::sendAction) - .launchIn(viewModelScope) - - premiumStateManager - .subscriptionStatusStateFlow - .map { PlanAction.Internal.SubscriptionStatusUpdateReceive(it) } - .onEach(::sendAction) - .launchIn(viewModelScope) - - premiumStateManager - .upgradeLifecycleStateFlow - .map { PlanAction.Internal.UpgradeLifecycleStateReceive(it) } + merge( + authRepository.userStateFlow.map { PlanAction.Internal.UserStateUpdateReceive(it) }, + specialCircumstanceManager + .specialCircumstanceStateFlow + .map { PlanAction.Internal.SpecialCircumstanceReceive(it) }, + premiumStateManager + .subscriptionStatusStateFlow + .map { PlanAction.Internal.SubscriptionStatusUpdateReceive(it) }, + premiumStateManager + .upgradeLifecycleStateFlow + .map { PlanAction.Internal.UpgradeLifecycleStateReceive(it) }, + ) + .onEach { + // Wait until we are in the Content state so we can update everything appropriately + mutableStateFlow.first { it.viewState is PlanState.ViewState.Content } + } .onEach(::sendAction) .launchIn(viewModelScope) - onFreeCloudContent { - viewModelScope.launch { - sendAction( - PlanAction.Internal.PricingResultReceive( - result = billingRepository.getPremiumPlanPricing(), - ), - ) + when { + state.showsPremiumView -> { + // We are loading the premium data. + viewModelScope.launch { + sendAction( + PlanAction.Internal.SubscriptionResultReceive( + result = billingRepository.getSubscription(), + ), + ) + } } - } - onPremiumContent { - mutableStateFlow.update { - it.copy( - dialogState = PlanState.DialogState.Loading( - message = BitwardenString.loading_subscription.asText(), - ), - ) + state.isSelfHosted -> { + // Nothing to load, we are good to go. } - viewModelScope.launch { - sendAction( - PlanAction.Internal.SubscriptionResultReceive( - result = billingRepository.getSubscription(), - ), - ) + + else -> { + // We are loading the plan details. + viewModelScope.launch { + sendAction( + PlanAction.Internal.PricingResultReceive( + result = billingRepository.getPremiumPlanPricing(), + ), + ) + } } } } @@ -384,7 +391,7 @@ class PlanViewModel @Inject constructor( private fun handleRetrySubscriptionClick() { mutableStateFlow.update { it.copy( - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading_subscription.asText(), ), ) @@ -415,15 +422,7 @@ class PlanViewModel @Inject constructor( SubscriptionResult.NotFound -> { mutableStateFlow.update { it.copy( - viewState = PlanState.ViewState.Free.Cloud( - rate = PLACEHOLDER_TEXT, - checkoutUrl = null, - isAwaitingPremiumStatus = false, - isPremiumUpgradePending = premiumStateManager - .upgradeLifecycleStateFlow - .value is UpgradeLifecycleState.UpgradePending, - ), - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading.asText(), ), ) @@ -440,11 +439,9 @@ class PlanViewModel @Inject constructor( is SubscriptionResult.Error -> { mutableStateFlow.update { it.copy( - dialogState = PlanState.DialogState.SubscriptionError( - title = BitwardenString.subscription_error.asText(), - message = BitwardenString - .trouble_loading_subscription - .asText(), + viewState = PlanState.ViewState.Error( + message = BitwardenString.trouble_loading_subscription.asText(), + type = PlanState.ViewState.Error.Type.SUBSCRIPTION, ), ) } @@ -471,15 +468,13 @@ class PlanViewModel @Inject constructor( private fun handleSubscriptionStatusUpdateReceive( action: PlanAction.Internal.SubscriptionStatusUpdateReceive, ) { - val status = (action.state as? SubscriptionStatusState.Available)?.status - ?: return + val status = (action.state as? SubscriptionStatusState.Available)?.status ?: return if (!status.isPremiumViewEligible()) return onFreeCloudContent { freeState -> if (freeState.isAwaitingPremiumStatus) return@onFreeCloudContent mutableStateFlow.update { it.copy( - viewState = PlanState.ViewState.Premium(), - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading_subscription.asText(), ), ) @@ -501,10 +496,15 @@ class PlanViewModel @Inject constructor( private fun handleUserStateUpdateReceive( action: PlanAction.Internal.UserStateUpdateReceive, ) { + val isPremium = action.userState?.activeAccount?.isPremium == true + mutableStateFlow.update { + it.copy( + showsPremiumView = isPremium || + premiumStateManager.subscriptionStatusStateFlow.value.isPremiumViewEligible(), + ) + } onFreeCloudContent { freeState -> if (!freeState.isAwaitingPremiumStatus) return@onFreeCloudContent - - val isPremium = action.userState?.activeAccount?.isPremium == true if (isPremium) { onPremiumUpgradeSuccess() } @@ -528,7 +528,7 @@ class PlanViewModel @Inject constructor( specialCircumstanceManager.specialCircumstance = null mutableStateFlow.update { it.copy( - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading_subscription.asText(), ), ) @@ -622,8 +622,7 @@ class PlanViewModel @Inject constructor( onFreeCloudContent { mutableStateFlow.update { it.copy( - viewState = PlanState.ViewState.Premium(), - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading_subscription.asText(), ), ) @@ -647,17 +646,16 @@ class PlanViewModel @Inject constructor( ) { when (val result = action.result) { is PremiumPlanPricingResult.Success -> { - val formattedRate = currencyFormatter - .format(result.annualPrice / MONTHS_PER_YEAR) - mutableStateFlow.update { currentState -> - val updatedViewState = when (val vs = currentState.viewState) { - is PlanState.ViewState.Free.Cloud -> vs.copy(rate = formattedRate) - is PlanState.ViewState.Free.SelfHosted, - is PlanState.ViewState.Premium, - -> vs - } - currentState.copy( - viewState = updatedViewState, + mutableStateFlow.update { + it.copy( + viewState = PlanState.ViewState.Content.Free.Cloud( + rate = currencyFormatter.format(result.annualPrice / MONTHS_PER_YEAR), + checkoutUrl = null, + isAwaitingPremiumStatus = false, + isPremiumUpgradePending = premiumStateManager + .upgradeLifecycleStateFlow + .value is UpgradeLifecycleState.UpgradePending, + ), dialogState = null, ) } @@ -666,10 +664,10 @@ class PlanViewModel @Inject constructor( is PremiumPlanPricingResult.Error -> { mutableStateFlow.update { it.copy( - dialogState = PlanState.DialogState.GetPricingError( - title = BitwardenString.pricing_unavailable.asText(), + viewState = PlanState.ViewState.Error( message = result.errorMessage?.asText() - ?: BitwardenString.generic_error_message.asText(), + ?: BitwardenString.pricing_unavailable.asText(), + type = PlanState.ViewState.Error.Type.PRICING_UNAVAILABLE, ), ) } @@ -680,7 +678,7 @@ class PlanViewModel @Inject constructor( private fun handleRetryPricingClick() { mutableStateFlow.update { it.copy( - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading.asText(), ), ) @@ -695,25 +693,25 @@ class PlanViewModel @Inject constructor( } private inline fun onFreeCloudContent( - block: (PlanState.ViewState.Free.Cloud) -> Unit, + block: (PlanState.ViewState.Content.Free.Cloud) -> Unit, ) { - (state.viewState as? PlanState.ViewState.Free.Cloud)?.let(block) + (state.viewState as? PlanState.ViewState.Content.Free.Cloud)?.let(block) } private inline fun onPremiumContent( - block: (PlanState.ViewState.Premium) -> Unit, + block: (PlanState.ViewState.Content.Premium) -> Unit, ) { - (state.viewState as? PlanState.ViewState.Premium)?.let(block) + (state.viewState as? PlanState.ViewState.Content.Premium)?.let(block) } - private fun SubscriptionInfo.toPremiumViewState(): PlanState.ViewState.Premium { + private fun SubscriptionInfo.toPremiumViewState(): PlanState.ViewState.Content.Premium { val formattedTotal = currencyFormatter.format(nextChargeTotal) val formattedDate = nextCharge?.toLocalizedDate() val formattedCancelAt = cancelAt?.toLocalizedDate() val formattedCanceled = canceledDate?.toLocalizedDate() val formattedSuspension = suspensionDate?.toLocalizedDate() - return PlanState.ViewState.Premium( + return PlanState.ViewState.Content.Premium( status = status, billingAmountText = seatsCost.toBillingAmountText(cadence, currencyFormatter), storageCostText = storageCost.toPresentMoneyText(currencyFormatter), @@ -760,6 +758,8 @@ data class PlanState( val planMode: PlanMode, val viewState: ViewState, val dialogState: DialogState?, + val showsPremiumView: Boolean, + val isSelfHosted: Boolean, ) : Parcelable { /** @@ -787,10 +787,7 @@ data class PlanState( */ @get:StringRes val title: Int - get() = when (viewState) { - is ViewState.Free -> BitwardenString.upgrade_to_premium - is ViewState.Premium -> BitwardenString.plan - } + get() = if (showsPremiumView) BitwardenString.plan else BitwardenString.upgrade_to_premium /** * Models the content state of the plan screen. @@ -798,65 +795,93 @@ data class PlanState( sealed class ViewState : Parcelable { /** - * Free user view — shows the upgrade flow for cloud accounts or a - * "manage on web vault" info card for self-hosted accounts. + * Displays a loading state. */ - sealed class Free : ViewState() { + @Parcelize + data class Loading(val message: Text) : ViewState() + /** + * Displays an error state. + */ + @Parcelize + data class Error( + val message: Text, + val type: Type, + ) : ViewState() { /** - * Free user on a cloud-hosted environment — shows upgrade pricing - * and feature list. + * The specific type of error this represents. */ - @Parcelize - data class Cloud( - val rate: String, - val checkoutUrl: String?, - val isAwaitingPremiumStatus: Boolean, - val isPremiumUpgradePending: Boolean, - ) : Free() + enum class Type { + PRICING_UNAVAILABLE, + SUBSCRIPTION, + } + } + /** + * Displays a plan content. + */ + sealed class Content : ViewState() { /** - * Free user on a self-hosted environment — Stripe checkout is - * unavailable, so the screen redirects the user to manage their - * subscription on the web vault. + * Free user view — shows the upgrade flow for cloud accounts or a + * "manage on web vault" info card for self-hosted accounts. + */ + sealed class Free : Content() { + + /** + * Free user on a cloud-hosted environment — shows upgrade pricing + * and feature list. + */ + @Parcelize + data class Cloud( + val rate: String, + val checkoutUrl: String?, + val isAwaitingPremiumStatus: Boolean, + val isPremiumUpgradePending: Boolean, + ) : Free() + + /** + * Free user on a self-hosted environment — Stripe checkout is + * unavailable, so the screen redirects the user to manage their + * subscription on the web vault. + */ + @Parcelize + data object SelfHosted : Free() + } + + /** + * Premium user view — shows subscription details and management options. + * + * 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 object SelfHosted : Free() + data class Premium( + val status: PremiumSubscriptionStatus? = null, + 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, + val canceledDateText: String? = null, + val suspensionDateText: String? = null, + val gracePeriodDays: Int? = null, + val showCancelButton: Boolean = false, + ) : Content() } - - /** - * Premium user view — shows subscription details and management options. - * - * 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 = "".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, - val canceledDateText: String? = null, - val suspensionDateText: String? = null, - val gracePeriodDays: Int? = null, - val showCancelButton: Boolean = false, - ) : ViewState() } /** @@ -878,15 +903,6 @@ data class PlanState( @Parcelize data object CheckoutError : DialogState() - /** - * Error dialog shown when pricing information cannot be retrieved. - */ - @Parcelize - data class GetPricingError( - val title: Text, - val message: Text, - ) : DialogState() - /** * Waiting dialog shown when the user returns from checkout without * completing payment. @@ -920,15 +936,6 @@ data class PlanState( */ @Parcelize data object PortalError : DialogState() - - /** - * Error dialog shown when subscription details cannot be loaded. - */ - @Parcelize - data class SubscriptionError( - val title: Text, - val message: Text, - ) : DialogState() } } 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 924a7a4eaa8..d98ea1a4c8e 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 @@ -388,74 +388,95 @@ class PlanScreenTest : BitwardenComposeTest() { // endregion PendingUpgrade dialog tests - // region GetPricingError dialog tests + // region Loading and Error content @Test - fun `get pricing error dialog should render when dialogState is GetPricingError`() { - val title = "An error has occurred".asText() - val message = "Unable to retrieve pricing.".asText() + fun `loading content should render message when viewState is Loading`() { + mutableStateFlow.update { + it.copy( + viewState = PlanState.ViewState.Loading( + message = BitwardenString.loading_subscription.asText(), + ), + ) + } + composeTestRule + .onNodeWithText("Loading subscription…") + .assertIsDisplayed() + } + @Test + fun `error content should render message and try again button when viewState is Error`() { composeTestRule - .onAllNodesWithText("An error has occurred") - .filterToOne(hasAnyAncestor(isDialog())) + .onNodeWithText("Pricing unavailable") .assertDoesNotExist() mutableStateFlow.update { it.copy( - dialogState = PlanState.DialogState.GetPricingError( - title = title, - message = message, + viewState = PlanState.ViewState.Error( + message = BitwardenString.pricing_unavailable.asText(), + type = PlanState.ViewState.Error.Type.PRICING_UNAVAILABLE, ), ) } composeTestRule - .onAllNodesWithText("An error has occurred") - .filterToOne(hasAnyAncestor(isDialog())) - .assertExists() + .onNodeWithText("Pricing unavailable") + .assertIsDisplayed() composeTestRule - .onAllNodesWithText("Unable to retrieve pricing.") - .filterToOne(hasAnyAncestor(isDialog())) - .assertExists() + .onNodeWithText("Try again") + .assertIsDisplayed() } @Test - fun `get pricing error dialog try again click should send RetryPricingClick action`() { + fun `error content try again click for PRICING_UNAVAILABLE should send RetryPricingClick`() { mutableStateFlow.update { it.copy( - dialogState = PlanState.DialogState.GetPricingError( - title = "An error has occurred".asText(), - message = "Unable to retrieve pricing.".asText(), + viewState = PlanState.ViewState.Error( + message = BitwardenString.pricing_unavailable.asText(), + type = PlanState.ViewState.Error.Type.PRICING_UNAVAILABLE, ), ) } composeTestRule - .onAllNodesWithText("Try again") - .filterToOne(hasAnyAncestor(isDialog())) + .onNodeWithText("Try again") .performClick() verify { viewModel.trySendAction(PlanAction.RetryPricingClick) } } @Test - fun `get pricing error dialog close click should send ClosePricingErrorClick action`() { + fun `error content should render subscription message when type is SUBSCRIPTION`() { mutableStateFlow.update { it.copy( - dialogState = PlanState.DialogState.GetPricingError( - title = "An error has occurred".asText(), - message = "Unable to retrieve pricing.".asText(), + viewState = PlanState.ViewState.Error( + message = BitwardenString.trouble_loading_subscription.asText(), + type = PlanState.ViewState.Error.Type.SUBSCRIPTION, ), ) } composeTestRule - .onAllNodesWithText("Close") - .filterToOne(hasAnyAncestor(isDialog())) - .performClick() - verify { - viewModel.trySendAction(PlanAction.ClosePricingErrorClick) + .onNodeWithText( + "We couldn’t load your subscription details. Please try again.", + ) + .assertIsDisplayed() + } + + @Test + fun `error content try again click for SUBSCRIPTION should send RetrySubscriptionClick`() { + mutableStateFlow.update { + it.copy( + viewState = PlanState.ViewState.Error( + message = BitwardenString.trouble_loading_subscription.asText(), + type = PlanState.ViewState.Error.Type.SUBSCRIPTION, + ), + ) } + composeTestRule + .onNodeWithText("Try again") + .performClick() + verify { viewModel.trySendAction(PlanAction.RetrySubscriptionClick) } } - // endregion GetPricingError dialog tests + // endregion Loading and Error content // region Premium content rendering @@ -1067,80 +1088,6 @@ class PlanScreenTest : BitwardenComposeTest() { // region Premium-flow dialogs - @Test - fun `subscription error dialog should render when dialogState is SubscriptionError`() { - val title = "An error has occurred".asText() - val message = "Unable to load subscription.".asText() - - composeTestRule - .onAllNodesWithText("An error has occurred") - .filterToOne(hasAnyAncestor(isDialog())) - .assertDoesNotExist() - - mutableStateFlow.update { - it.copy( - viewState = PlanState.ViewState.Premium(), - dialogState = PlanState.DialogState.SubscriptionError( - title = title, - message = message, - ), - ) - } - - composeTestRule - .onAllNodesWithText("An error has occurred") - .filterToOne(hasAnyAncestor(isDialog())) - .assertExists() - composeTestRule - .onAllNodesWithText("Unable to load subscription.") - .filterToOne(hasAnyAncestor(isDialog())) - .assertExists() - composeTestRule - .onAllNodesWithText("Try again") - .filterToOne(hasAnyAncestor(isDialog())) - .assertExists() - composeTestRule - .onAllNodesWithText("Close") - .filterToOne(hasAnyAncestor(isDialog())) - .assertExists() - } - - @Test - fun `subscription error dialog try again click should send RetrySubscriptionClick action`() { - mutableStateFlow.update { - it.copy( - viewState = PlanState.ViewState.Premium(), - dialogState = PlanState.DialogState.SubscriptionError( - title = "An error has occurred".asText(), - message = "Unable to load subscription.".asText(), - ), - ) - } - composeTestRule - .onAllNodesWithText("Try again") - .filterToOne(hasAnyAncestor(isDialog())) - .performClick() - verify { viewModel.trySendAction(PlanAction.RetrySubscriptionClick) } - } - - @Test - fun `subscription error dialog close click should send BackClick action`() { - mutableStateFlow.update { - it.copy( - viewState = PlanState.ViewState.Premium(), - dialogState = PlanState.DialogState.SubscriptionError( - title = "An error has occurred".asText(), - message = "Unable to load subscription.".asText(), - ), - ) - } - composeTestRule - .onAllNodesWithText("Close") - .filterToOne(hasAnyAncestor(isDialog())) - .performClick() - verify { viewModel.trySendAction(PlanAction.BackClick) } - } - @Test fun `loading portal dialog should render when dialogState is LoadingPortal`() { composeTestRule @@ -1294,7 +1241,7 @@ class PlanScreenTest : BitwardenComposeTest() { @Test fun `manage subscription info callout should render when self-hosted free`() { mutableStateFlow.update { - it.copy(viewState = PlanState.ViewState.Free.SelfHosted) + it.copy(viewState = PlanState.ViewState.Content.Free.SelfHosted) } composeTestRule .onNodeWithText( @@ -1310,7 +1257,7 @@ class PlanScreenTest : BitwardenComposeTest() { @Test fun `premium features header should render when self-hosted free`() { mutableStateFlow.update { - it.copy(viewState = PlanState.ViewState.Free.SelfHosted) + it.copy(viewState = PlanState.ViewState.Content.Free.SelfHosted) } composeTestRule .onNodeWithText("Unlock more advanced features with a Premium plan.") @@ -1320,7 +1267,7 @@ class PlanScreenTest : BitwardenComposeTest() { @Test fun `premium feature list items should render when self-hosted free`() { mutableStateFlow.update { - it.copy(viewState = PlanState.ViewState.Free.SelfHosted) + it.copy(viewState = PlanState.ViewState.Content.Free.SelfHosted) } composeTestRule .onNodeWithText("Built-in authenticator") @@ -1371,16 +1318,18 @@ class PlanScreenTest : BitwardenComposeTest() { private val DEFAULT_FREE_STATE = PlanState( planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.65", checkoutUrl = null, isAwaitingPremiumStatus = false, isPremiumUpgradePending = false, ), dialogState = null, + showsPremiumView = false, + isSelfHosted = false, ) -private val DEFAULT_PREMIUM_VIEW_STATE = PlanState.ViewState.Premium( +private val DEFAULT_PREMIUM_VIEW_STATE = PlanState.ViewState.Content.Premium( status = PremiumSubscriptionStatus.ACTIVE, billingAmountText = BitwardenString.billing_rate_per_year.asText("$19.80"), storageCostText = "$24.00", 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 779d83685c3..f5152017411 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 @@ -144,7 +144,7 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = null, isAwaitingPremiumStatus = true, @@ -170,7 +170,9 @@ class PlanViewModelTest : BaseViewModelTest() { ), ) - val viewModel = createViewModel() + // The premium subscription must resolve so the screen reaches a Content state and + // the special-circumstance flow is processed. + val viewModel = createViewModel(subscriptionResult = SUBSCRIPTION_SUCCESS_ACTIVE) viewModel.eventFlow.test { mutableSpecialCircumstanceStateFlow.value = @@ -225,7 +227,7 @@ class PlanViewModelTest : BaseViewModelTest() { ) assertEquals( DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = checkoutUrl, isAwaitingPremiumStatus = false, @@ -316,7 +318,7 @@ class PlanViewModelTest : BaseViewModelTest() { ) assertEquals( DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = checkoutUrl, isAwaitingPremiumStatus = false, @@ -389,7 +391,7 @@ class PlanViewModelTest : BaseViewModelTest() { fun `GoBackClick should emit LaunchBrowser with checkout URL when URL is available`() = runTest { val checkoutUrl = "https://checkout.stripe.com/session123" - val freeState = PlanState.ViewState.Free.Cloud( + val freeState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = checkoutUrl, isAwaitingPremiumStatus = false, @@ -438,7 +440,7 @@ class PlanViewModelTest : BaseViewModelTest() { runTest { val viewModel = createViewModel( initialState = DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = null, isAwaitingPremiumStatus = true, @@ -481,7 +483,7 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = null, isAwaitingPremiumStatus = true, @@ -499,13 +501,14 @@ class PlanViewModelTest : BaseViewModelTest() { ), ) - // State transitions to Premium with subscription Loading. + // State transitions to a subscription Loading view state. assertEquals( DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Premium(), - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading_subscription.asText(), ), + dialogState = PlanState.DialogState.WaitingForPayment, + showsPremiumView = true, ), stateFlow.awaitItem(), ) @@ -537,7 +540,7 @@ class PlanViewModelTest : BaseViewModelTest() { // Sync completes without premium — PendingUpgrade shown. assertEquals( DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = null, isAwaitingPremiumStatus = true, @@ -562,7 +565,7 @@ class PlanViewModelTest : BaseViewModelTest() { fun `ContinueClick dismisses the PendingUpgrade dialog and navigates back`() = runTest { val viewModel = createViewModel( initialState = DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = null, isAwaitingPremiumStatus = true, @@ -590,7 +593,7 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = null, isAwaitingPremiumStatus = false, @@ -611,7 +614,7 @@ class PlanViewModelTest : BaseViewModelTest() { runTest { val viewModel = createViewModel( initialState = DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = null, isAwaitingPremiumStatus = true, @@ -687,8 +690,10 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( PlanState( planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Free.SelfHosted, + viewState = PlanState.ViewState.Content.Free.SelfHosted, dialogState = null, + showsPremiumView = false, + isSelfHosted = true, ), awaitItem(), ) @@ -720,19 +725,7 @@ class PlanViewModelTest : BaseViewModelTest() { val viewModel = createViewModel() viewModel.stateFlow.test { - assertEquals( - PlanState( - planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Free.Cloud( - rate = "$1.67", - checkoutUrl = null, - isAwaitingPremiumStatus = false, - isPremiumUpgradePending = false, - ), - dialogState = null, - ), - awaitItem(), - ) + assertEquals(DEFAULT_FREE_STATE, awaitItem()) } coVerify(exactly = 1) { mockBillingRepository.getPremiumPlanPricing() @@ -744,7 +737,7 @@ class PlanViewModelTest : BaseViewModelTest() { // region Pricing fetch @Test - fun `initial state before pricing fetch resolves should show placeholder rate`() = + fun `initial state before pricing fetch resolves should show Loading view state`() = runTest { val viewModel = createViewModel(pricingResult = null) @@ -752,13 +745,12 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( PlanState( planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Free.Cloud( - rate = "--", - checkoutUrl = null, - isAwaitingPremiumStatus = false, - isPremiumUpgradePending = false, + viewState = PlanState.ViewState.Loading( + message = BitwardenString.loading.asText(), ), dialogState = null, + showsPremiumView = false, + isSelfHosted = false, ), awaitItem(), ) @@ -766,7 +758,7 @@ class PlanViewModelTest : BaseViewModelTest() { } @Test - fun `pricing fetch failure should show GetPricingError dialog`() = + fun `pricing fetch failure should show Error view state`() = runTest { val viewModel = createViewModel( pricingResult = PremiumPlanPricingResult.Error( @@ -778,16 +770,13 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( PlanState( planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Free.Cloud( - rate = "--", - checkoutUrl = null, - isAwaitingPremiumStatus = false, - isPremiumUpgradePending = false, - ), - dialogState = PlanState.DialogState.GetPricingError( - title = BitwardenString.pricing_unavailable.asText(), - message = BitwardenString.generic_error_message.asText(), + viewState = PlanState.ViewState.Error( + message = BitwardenString.pricing_unavailable.asText(), + type = PlanState.ViewState.Error.Type.PRICING_UNAVAILABLE, ), + dialogState = null, + showsPremiumView = false, + isSelfHosted = false, ), awaitItem(), ) @@ -807,16 +796,13 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( PlanState( planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Free.Cloud( - rate = "--", - checkoutUrl = null, - isAwaitingPremiumStatus = false, - isPremiumUpgradePending = false, - ), - dialogState = PlanState.DialogState.GetPricingError( - title = BitwardenString.pricing_unavailable.asText(), - message = BitwardenString.generic_error_message.asText(), + viewState = PlanState.ViewState.Error( + message = BitwardenString.pricing_unavailable.asText(), + type = PlanState.ViewState.Error.Type.PRICING_UNAVAILABLE, ), + dialogState = null, + showsPremiumView = false, + isSelfHosted = false, ), awaitItem(), ) @@ -831,15 +817,12 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( PlanState( planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Free.Cloud( - rate = "--", - checkoutUrl = null, - isAwaitingPremiumStatus = false, - isPremiumUpgradePending = false, - ), - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading.asText(), ), + dialogState = null, + showsPremiumView = false, + isSelfHosted = false, ), awaitItem(), ) @@ -851,7 +834,7 @@ class PlanViewModelTest : BaseViewModelTest() { } @Test - fun `ClosePricingErrorClick should clear dialog and emit NavigateBack`() = + fun `ClosePricingErrorClick should emit NavigateBack`() = runTest { val viewModel = createViewModel( pricingResult = PremiumPlanPricingResult.Error( @@ -859,42 +842,12 @@ class PlanViewModelTest : BaseViewModelTest() { ), ) - viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow -> - assertEquals( - PlanState( - planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Free.Cloud( - rate = "--", - checkoutUrl = null, - isAwaitingPremiumStatus = false, - isPremiumUpgradePending = false, - ), - dialogState = PlanState.DialogState.GetPricingError( - title = BitwardenString.pricing_unavailable.asText(), - message = BitwardenString.generic_error_message.asText(), - ), - ), - stateFlow.awaitItem(), - ) - + viewModel.eventFlow.test { viewModel.trySendAction(PlanAction.ClosePricingErrorClick) - assertEquals( - PlanState( - planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Free.Cloud( - rate = "--", - checkoutUrl = null, - isAwaitingPremiumStatus = false, - isPremiumUpgradePending = false, - ), - dialogState = null, - ), - stateFlow.awaitItem(), - ) assertEquals( PlanEvent.NavigateBack, - eventFlow.awaitItem(), + awaitItem(), ) } } @@ -926,7 +879,7 @@ class PlanViewModelTest : BaseViewModelTest() { // region Premium user path @Test - fun `initial state should be Premium ViewState with loading dialog for premium user`() = + fun `initial state should be Loading ViewState for premium user`() = runTest { markUserPremium() @@ -1005,15 +958,12 @@ class PlanViewModelTest : BaseViewModelTest() { ) val loadingState = awaitItem() assertEquals( - PlanState.ViewState.Premium(), - loadingState.viewState, - ) - assertEquals( - PlanState.DialogState.Loading( + PlanState.ViewState.Loading( message = BitwardenString.loading_subscription.asText(), ), - loadingState.dialogState, + loadingState.viewState, ) + assertEquals(null, loadingState.dialogState) val loadedState = awaitItem() assertEquals( DEFAULT_PREMIUM_ACTIVE_VIEW_STATE.copy( @@ -1036,13 +986,15 @@ class PlanViewModelTest : BaseViewModelTest() { ) viewModel.stateFlow.test { - assertEquals(DEFAULT_FREE_STATE, awaitItem()) + // The account is premium, so showsPremiumView stays true even though the + // missing subscription drops the screen back to the Free Cloud upgrade view. + assertEquals(DEFAULT_FREE_STATE.copy(showsPremiumView = true), awaitItem()) } } @Suppress("MaxLineLength") @Test - fun `SubscriptionResultReceive NotFound keeps Loading dialog up while pricing fetch is pending`() = + fun `SubscriptionResultReceive NotFound keeps Loading view state up while pricing fetch is pending`() = runTest { markUserPremium() @@ -1053,14 +1005,8 @@ class PlanViewModelTest : BaseViewModelTest() { viewModel.stateFlow.test { assertEquals( - DEFAULT_FREE_STATE.copy( - viewState = PlanState.ViewState.Free.Cloud( - rate = "--", - checkoutUrl = null, - isAwaitingPremiumStatus = false, - isPremiumUpgradePending = false, - ), - dialogState = PlanState.DialogState.Loading( + DEFAULT_PREMIUM_LOADING_STATE.copy( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading.asText(), ), ), @@ -1447,7 +1393,7 @@ class PlanViewModelTest : BaseViewModelTest() { } @Test - fun `SubscriptionResultReceive Error should show SubscriptionError dialog`() = runTest { + fun `SubscriptionResultReceive Error should show Error view state`() = runTest { markUserPremium() val viewModel = createViewModel( @@ -1457,11 +1403,9 @@ class PlanViewModelTest : BaseViewModelTest() { viewModel.stateFlow.test { assertEquals( DEFAULT_PREMIUM_LOADING_STATE.copy( - dialogState = PlanState.DialogState.SubscriptionError( - title = BitwardenString.subscription_error.asText(), - message = BitwardenString - .trouble_loading_subscription - .asText(), + viewState = PlanState.ViewState.Error( + message = BitwardenString.trouble_loading_subscription.asText(), + type = PlanState.ViewState.Error.Type.SUBSCRIPTION, ), ), awaitItem(), @@ -1485,7 +1429,7 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( DEFAULT_PREMIUM_LOADED_STATE.copy( - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading_subscription.asText(), ), ), @@ -1666,7 +1610,7 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( DEFAULT_PREMIUM_LOADED_STATE.copy( - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading_subscription.asText(), ), ), @@ -1704,7 +1648,7 @@ class PlanViewModelTest : BaseViewModelTest() { assertEquals( DEFAULT_PREMIUM_LOADED_STATE.copy( - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading_subscription.asText(), ), ), @@ -1862,13 +1806,15 @@ private val DEFAULT_USER_STATE = UserState( private val DEFAULT_FREE_STATE = PlanState( planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Free.Cloud( + viewState = PlanState.ViewState.Content.Free.Cloud( rate = "$1.67", checkoutUrl = null, isAwaitingPremiumStatus = false, isPremiumUpgradePending = false, ), dialogState = null, + showsPremiumView = false, + isSelfHosted = false, ) private const val ANNUAL_PRICE = 19.99 @@ -1900,7 +1846,7 @@ private val DEFAULT_PRICING_SUCCESS = PremiumPlanPricingResult.Success( annualPrice = ANNUAL_PRICE, ) -private val DEFAULT_PREMIUM_ACTIVE_VIEW_STATE = PlanState.ViewState.Premium( +private val DEFAULT_PREMIUM_ACTIVE_VIEW_STATE = PlanState.ViewState.Content.Premium( status = PremiumSubscriptionStatus.ACTIVE, billingAmountText = BitwardenString.billing_rate_per_year.asText("$19.80"), storageCostText = "$24.00", @@ -1916,12 +1862,16 @@ private val DEFAULT_PREMIUM_LOADED_STATE = PlanState( planMode = PlanMode.Modal, viewState = DEFAULT_PREMIUM_ACTIVE_VIEW_STATE, dialogState = null, + showsPremiumView = true, + isSelfHosted = false, ) private val DEFAULT_PREMIUM_LOADING_STATE = PlanState( planMode = PlanMode.Modal, - viewState = PlanState.ViewState.Premium(), - dialogState = PlanState.DialogState.Loading( + viewState = PlanState.ViewState.Loading( message = BitwardenString.loading_subscription.asText(), ), + dialogState = null, + showsPremiumView = true, + isSelfHosted = false, ) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 740066c9f66..f620c2f4f0f 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1324,7 +1324,6 @@ Do you want to switch to this account? Loading portal… Something went wrong We had trouble loading the management portal, so try again. - Subscription error We couldn’t load your subscription details. Please try again. View bank account View license