From 31011b5789fa2b47e16bc50a708d047a3dda34d0 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Wed, 20 May 2026 15:19:58 -0400 Subject: [PATCH] [PM-37289] fix: Refresh archive row after premium upgrade (#6949) --- .../ui/vault/feature/vault/VaultViewModel.kt | 14 ++- .../vault/feature/vault/VaultViewModelTest.kt | 99 ++++++++++++++++++- 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index 862abea59c..7f776fcf24 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -1391,6 +1391,8 @@ class VaultViewModel @Inject constructor( .any(), ) val appBarTitle = vaultFilterData.toAppBarTitle() + val previousIsPremium = state.isPremium + val nextIsPremium = userState.activeAccount.isPremium mutableStateFlow.update { val accountSummaries = userState.toAccountSummaries() @@ -1401,10 +1403,20 @@ class VaultViewModel @Inject constructor( avatarColorString = activeAccountSummary.avatarColorHex, accountSummaries = accountSummaries, vaultFilterData = vaultFilterData, - isPremium = userState.activeAccount.isPremium, + isPremium = nextIsPremium, showImportActionCard = firstTimeState.showImportLoginsCard, ) } + + // Archive UI fields (count, lock icon, "Premium required" subtext) are precomputed + // from isPremium when the viewState is built. Recompute when isPremium transitions + // so the row reflects the new entitlement immediately after upgrade. + if (previousIsPremium != nextIsPremium) { + updateViewState( + vaultData = vaultRepository.vaultDataStateFlow.value, + validTotpIds = state.validTotpIds, + ) + } } private fun handleVaultDataReceive(action: VaultAction.Internal.VaultDataReceive) { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index 611f306bcc..f343617e35 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -49,17 +49,17 @@ import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManage import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockBankAccountView -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockDriversLicenseView -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPassportView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCardListView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCardView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockDecryptCipherListResult +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockDriversLicenseView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginListView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPassportView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipher import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult @@ -2429,6 +2429,101 @@ class VaultViewModelTest : BaseViewModelTest() { ) } + @Test + fun `isPremium transitioning from false to true should refresh archive viewState fields`() = + runTest { + mutableUserStateFlow.update { + DEFAULT_USER_STATE.copy( + accounts = listOf( + DEFAULT_ACTIVE_ACCOUNT.copy(isPremium = false), + DEFAULT_INACTIVE_ACCOUNT, + ), + ) + } + mutableVaultDataStateFlow.update { + DataState.Loaded( + VaultData( + decryptCipherListResult = createMockDecryptCipherListResult( + number = 1, + successes = listOf( + createMockCipherListView(number = 1, isArchived = false), + ), + failures = emptyList(), + ), + collectionViewList = emptyList(), + folderViewList = emptyList(), + sendViewList = emptyList(), + ), + ) + } + val viewModel = createViewModel() + + assertEquals( + VaultState.ViewState.Content( + loginItemsCount = 1, + cardItemsCount = 0, + identityItemsCount = 0, + secureNoteItemsCount = 0, + favoriteItems = listOf(), + folderItems = listOf(), + collectionItems = listOf(), + noFolderItems = listOf(), + trashItemsCount = 0, + totpItemsCount = 0, + itemTypesCount = CipherType.entries.size, + sshKeyItemsCount = 0, + bankAccountItemsCount = 0, + licenseItemsCount = 0, + passportItemsCount = 0, + archivedItemsCount = null, + archiveSubText = BitwardenString.premium_subscription_required.asText(), + archiveEndIcon = BitwardenDrawable.ic_locked, + showCardGroup = true, + showBankAccountGroup = false, + showLicenseGroup = false, + showPassportGroup = false, + ), + viewModel.stateFlow.value.viewState, + ) + + mutableUserStateFlow.update { + DEFAULT_USER_STATE.copy( + accounts = listOf( + DEFAULT_ACTIVE_ACCOUNT.copy(isPremium = true), + DEFAULT_INACTIVE_ACCOUNT, + ), + ) + } + + assertEquals( + VaultState.ViewState.Content( + loginItemsCount = 1, + cardItemsCount = 0, + identityItemsCount = 0, + secureNoteItemsCount = 0, + favoriteItems = listOf(), + folderItems = listOf(), + collectionItems = listOf(), + noFolderItems = listOf(), + trashItemsCount = 0, + totpItemsCount = 0, + itemTypesCount = CipherType.entries.size, + sshKeyItemsCount = 0, + bankAccountItemsCount = 0, + licenseItemsCount = 0, + passportItemsCount = 0, + archivedItemsCount = 0, + archiveSubText = null, + archiveEndIcon = null, + showCardGroup = true, + showBankAccountGroup = false, + showLicenseGroup = false, + showPassportGroup = false, + ), + viewModel.stateFlow.value.viewState, + ) + } + @Test fun `TrashClick should emit NavigateToItemListing event with Trash type`() = runTest { val viewModel = createViewModel()