Skip to content

Commit 15e217b

Browse files
PM-31656, PM-31658, PM-31659: Address Archive feature bugs (#6473)
1 parent 7ec4faf commit 15e217b

File tree

11 files changed

+258
-69
lines changed

11 files changed

+258
-69
lines changed

app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/AuthenticatorBridgeRepositoryImpl.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ class AuthenticatorBridgeRepositoryImpl(
9090
// Vault is unlocked, query vault disk source for totp logins:
9191
val totpUris = vaultDiskSource
9292
.getTotpCiphers(userId = userId)
93-
// Filter out any deleted ciphers.
94-
.filter { it.deletedDate == null }
93+
// Filter out any deleted and archived ciphers.
94+
.filter { it.deletedDate == null && it.archivedDate == null }
9595
.mapNotNull {
9696
scopedVaultSdkSource
9797
.decryptCipher(userId = userId, cipher = it.toEncryptedSdkCipher())

app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ class CipherManagerImpl(
191191
userId = userId,
192192
cipher = it.toEncryptedNetworkCipherResponse(),
193193
)
194+
settingsDiskSource.storeIntroducingArchiveActionCardDismissed(
195+
userId = userId,
196+
isDismissed = true,
197+
)
194198
}
195199
.fold(
196200
onSuccess = { ArchiveCipherResult.Success },

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ private fun ActionCard(
501501
tint = BitwardenTheme.colorScheme.icon.secondary,
502502
)
503503
},
504-
onActionClick = vaultHandlers.archiveClick,
504+
onActionClick = { vaultHandlers.actionCardClick(actionCardState) },
505505
onDismissClick = { vaultHandlers.dismissActionCardClick(actionCardState) },
506506
modifier = modifier,
507507
)

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ class VaultViewModel @Inject constructor(
320320

321321
VaultAction.UpgradeToPremiumClick -> handleUpgradeToPremiumClick()
322322
is VaultAction.DismissActionCardClick -> handleDismissActionCardClick(action)
323+
is VaultAction.ActionCardClick -> handleActionCardClick(action)
323324
}
324325
}
325326

@@ -377,6 +378,15 @@ class VaultViewModel @Inject constructor(
377378
}
378379
}
379380

381+
private fun handleActionCardClick(action: VaultAction.ActionCardClick) {
382+
when (action.actionCard) {
383+
VaultState.ActionCardState.IntroducingArchive -> {
384+
settingsRepository.dismissIntroducingArchiveActionCard()
385+
sendEvent(VaultEvent.NavigateToItemListing(VaultItemListingType.Archive))
386+
}
387+
}
388+
}
389+
380390
private fun handleSelectAddItemType() {
381391
// If policy is enable for any organization, exclude the card option
382392
val excludedOptions = persistentListOfNotNull(
@@ -572,7 +582,18 @@ class VaultViewModel @Inject constructor(
572582
}
573583

574584
private fun handleArchiveClick() {
575-
sendEvent(VaultEvent.NavigateToItemListing(VaultItemListingType.Archive))
585+
val archivedItemsCount = (state.viewState as? VaultState.ViewState.Content)
586+
?.archivedItemsCount
587+
?: 0
588+
if (state.isPremium || archivedItemsCount > 0) {
589+
// We still navigate even if the user does not have premium, since they have previously
590+
// archived ciphers to view.
591+
sendEvent(VaultEvent.NavigateToItemListing(VaultItemListingType.Archive))
592+
} else {
593+
mutableStateFlow.update {
594+
it.copy(dialog = VaultState.DialogState.ArchiveRequiresPremium)
595+
}
596+
}
576597
}
577598

578599
private fun handleTrashClick() {
@@ -2189,6 +2210,13 @@ sealed class VaultAction {
21892210
val actionCard: VaultState.ActionCardState,
21902211
) : VaultAction()
21912212

2213+
/**
2214+
* User clicked the primary button on an action card.
2215+
*/
2216+
data class ActionCardClick(
2217+
val actionCard: VaultState.ActionCardState,
2218+
) : VaultAction()
2219+
21922220
/**
21932221
* Models actions that the [VaultViewModel] itself might send.
21942222
*/

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ data class VaultHandlers(
5252
val onDismissThirdPartyAutofillDialogClick: () -> Unit,
5353
val upgradeToPremiumClick: () -> Unit,
5454
val dismissActionCardClick: (VaultState.ActionCardState) -> Unit,
55+
val actionCardClick: (VaultState.ActionCardState) -> Unit,
5556
) {
5657
@Suppress("UndocumentedPublicClass")
5758
companion object {
@@ -151,6 +152,7 @@ data class VaultHandlers(
151152
dismissActionCardClick = {
152153
viewModel.trySendAction(VaultAction.DismissActionCardClick(it))
153154
},
155+
actionCardClick = { viewModel.trySendAction(VaultAction.ActionCardClick(it)) },
154156
)
155157
}
156158
}

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ fun VaultData.toViewState(
132132
noFolderItems.size < NO_FOLDER_ITEM_THRESHOLD
133133
val totpItems = activeCipherViews.filter { it.login?.totp != null }
134134
val cardCount = activeCipherViews.count { it.type is CipherListViewType.Card }
135+
val archiveCount = allCipherViews.count {
136+
it.archivedDate != null && it.deletedDate == null
137+
}
135138
VaultState.ViewState.Content(
136139
itemTypesCount = itemTypesCount,
137140
totpItemsCount = if (isPremium) {
@@ -215,15 +218,13 @@ fun VaultData.toViewState(
215218
)
216219
},
217220
trashItemsCount = allCipherViews.count { it.deletedDate != null },
218-
archivedItemsCount = allCipherViews
219-
.count { it.archivedDate != null && it.deletedDate == null }
220-
.takeIf { isPremium },
221+
archivedItemsCount = archiveCount.takeIf { isPremium || archiveCount > 0 },
221222
archiveEnabled = isArchiveEnabled,
222-
archiveEndIcon = BitwardenDrawable.ic_locked.takeUnless { isPremium },
223+
archiveEndIcon = BitwardenDrawable.ic_locked.takeIf { !isPremium && archiveCount == 0 },
223224
archiveSubText = BitwardenString
224225
.premium_subscription_required
225226
.asText()
226-
.takeUnless { isPremium },
227+
.takeIf { !isPremium && archiveCount == 0 },
227228
showCardGroup = cardCount != 0 || restrictItemTypesPolicyOrgIds.isEmpty(),
228229
)
229230
}

app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,13 @@ class FakeSettingsDiskSource(
491491
return storedAppResumeScreenData[userId]?.let { Json.decodeFromStringOrNull(it) }
492492
}
493493

494+
/**
495+
* Asserts that the stored introducing archive action card dismissed matches the [expected] one.
496+
*/
497+
fun assertIntroducingArchiveActionCardDismissed(userId: String, expected: Boolean?) {
498+
assertEquals(expected, storedIntroducingArchiveActionCardDismissed[userId])
499+
}
500+
494501
/**
495502
* Asserts that the stored last sync time matches the [expected] one.
496503
*/

app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/AuthenticatorBridgeRepositoryTest.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ class AuthenticatorBridgeRepositoryTest {
238238

239239
@Test
240240
@Suppress("MaxLineLength")
241-
fun `getSharedAccounts should unlock and re-lock vault for both users and filter out deleted ciphers`() =
241+
fun `getSharedAccounts should unlock and re-lock vault for both users and filter out archived and deleted ciphers`() =
242242
runTest {
243243
assertEquals(
244244
BOTH_ACCOUNT_SUCCESS,
@@ -499,20 +499,31 @@ private val USER_STATE_JSON = UserStateJson(
499499
private val USER_1_TOTP_CIPHER = mockk<SyncResponseJson.Cipher> {
500500
every { login?.totp } returns "encryptedTotp1"
501501
every { login?.username } returns "username"
502+
every { archivedDate } returns null
502503
every { deletedDate } returns null
503504
every { name } returns "cipher1"
504505
}
505506

506507
private val USER_1_DELETED_TOTP_CIPHER = mockk<SyncResponseJson.Cipher> {
507508
every { login?.totp } returns "encryptedTotp1Deleted"
508509
every { login?.username } returns "username"
510+
every { archivedDate } returns null
509511
every { deletedDate } returns ZonedDateTime.parse("2023-10-27T12:00:00Z")
510512
every { name } returns "cipher1"
511513
}
512514

515+
private val USER_1_ARCHIVED_TOTP_CIPHER = mockk<SyncResponseJson.Cipher> {
516+
every { login?.totp } returns "encryptedTotp1Deleted"
517+
every { login?.username } returns "username"
518+
every { archivedDate } returns ZonedDateTime.parse("2023-10-27T12:00:00Z")
519+
every { deletedDate } returns null
520+
every { name } returns "cipher1"
521+
}
522+
513523
private val USER_2_TOTP_CIPHER = mockk<SyncResponseJson.Cipher> {
514524
every { login?.totp } returns "encryptedTotp2"
515525
every { login?.username } returns "username"
526+
every { archivedDate } returns null
516527
every { deletedDate } returns null
517528
every { name } returns "cipher2"
518529
}
@@ -553,6 +564,7 @@ private val USER_2_SHARED_ACCOUNT = SharedAccountData.Account(
553564
private val USER_1_CIPHERS = listOf(
554565
USER_1_TOTP_CIPHER,
555566
USER_1_DELETED_TOTP_CIPHER,
567+
USER_1_ARCHIVED_TOTP_CIPHER,
556568
)
557569

558570
private val USER_2_CIPHERS = listOf(

app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,10 @@ class CipherManagerTest {
744744
cipher = createMockSdkCipher(number = 1, clock = clock),
745745
)
746746
val cipherView = createMockCipherView(number = 1)
747+
fakeSettingsDiskSource.storeIntroducingArchiveActionCardDismissed(
748+
userId = userId,
749+
isDismissed = null,
750+
)
747751
coEvery {
748752
vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView)
749753
} returns encryptionContext.asSuccess()
@@ -770,6 +774,10 @@ class CipherManagerTest {
770774
cipherView = cipherView,
771775
)
772776

777+
fakeSettingsDiskSource.assertIntroducingArchiveActionCardDismissed(
778+
userId = userId,
779+
expected = true,
780+
)
773781
assertEquals(ArchiveCipherResult.Success, result)
774782
}
775783

app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1527,7 +1527,11 @@ class VaultScreenTest : BitwardenComposeTest() {
15271527
.performClick()
15281528

15291529
verify(exactly = 1) {
1530-
viewModel.trySendAction(VaultAction.ArchiveClick)
1530+
viewModel.trySendAction(
1531+
VaultAction.ActionCardClick(
1532+
actionCard = VaultState.ActionCardState.IntroducingArchive,
1533+
),
1534+
)
15311535
}
15321536
}
15331537

0 commit comments

Comments
 (0)