PM-22477: Update the timestamp format for ciphers (#5352)

This commit is contained in:
David Perez
2025-06-12 13:49:20 -05:00
committed by GitHub
parent 3c86bb425b
commit 5adccca823
12 changed files with 193 additions and 110 deletions

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -28,7 +29,6 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
@@ -303,16 +303,33 @@ fun VaultItemCardContent(
}
}
item(key = "lastUpdated") {
item(key = "created") {
Spacer(modifier = Modifier.height(height = 16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
Text(
text = commonState.created(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.animateItem(),
.animateItem()
.testTag("CardItemCreated"),
)
}
item(key = "lastUpdated") {
Spacer(modifier = Modifier.height(height = 4.dp))
Text(
text = commonState.lastUpdated(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.animateItem()
.testTag("CardItemLastUpdated"),
)
}

View File

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -29,7 +30,6 @@ import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
@@ -355,16 +355,33 @@ fun VaultItemIdentityContent(
}
}
item(key = "lastUpdated") {
item(key = "created") {
Spacer(modifier = Modifier.height(height = 16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
Text(
text = commonState.created(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.animateItem(),
.animateItem()
.testTag("IdentityItemCreated"),
)
}
item(key = "lastUpdated") {
Spacer(modifier = Modifier.height(height = 4.dp))
Text(
text = commonState.lastUpdated(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.animateItem()
.testTag("IdentityItemLastUpdated"),
)
}

View File

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -34,7 +35,6 @@ import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
@@ -268,25 +268,43 @@ fun VaultItemLoginContent(
}
}
item(key = "lastUpdated") {
Spacer(modifier = Modifier.height(16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
item(key = "created") {
Spacer(modifier = Modifier.height(height = 16.dp))
Text(
text = commonState.created(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.animateItem()
.testTag("LoginItemCreated"),
)
}
item(key = "lastUpdated") {
Spacer(modifier = Modifier.height(height = 4.dp))
Text(
text = commonState.lastUpdated(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.animateItem(),
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.animateItem()
.testTag("LoginItemLastUpdated"),
)
}
loginItemState.passwordRevisionDate?.let { revisionDate ->
item(key = "revisionDate") {
Spacer(modifier = Modifier.height(height = 4.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_password_updated)}: ",
text = revisionDate,
Text(
text = revisionDate(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -18,7 +17,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
@@ -153,27 +151,34 @@ fun VaultItemSecureNoteContent(
}
}
item(key = "lastUpdated") {
item(key = "created") {
Spacer(modifier = Modifier.height(height = 16.dp))
Row(
Text(
text = commonState.created(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.semantics(mergeDescendants = true) { }
.animateItem(),
) {
Text(
text = "${stringResource(id = R.string.date_updated)}: ",
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
)
Text(
text = commonState.lastUpdated,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
)
}
.animateItem()
.testTag("SecureNoteItemCreated"),
)
}
item(key = "lastUpdated") {
Spacer(modifier = Modifier.height(height = 4.dp))
Text(
text = commonState.lastUpdated(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.animateItem()
.testTag("SecureNoteItemLastUpdated"),
)
}
commonState.passwordHistoryCount?.let { passwordHistoryCount ->

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -29,7 +30,6 @@ import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
import com.x8bit.bitwarden.ui.vault.feature.item.component.VaultItemUpdateText
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
@@ -235,11 +235,27 @@ fun VaultItemSshKeyContent(
}
}
item(key = "lastUpdated") {
item(key = "created") {
Spacer(modifier = Modifier.height(height = 16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
Text(
text = commonState.created(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.animateItem()
.testTag("SshKeyItemCreated"),
)
}
item(key = "lastUpdated") {
Spacer(modifier = Modifier.height(height = 4.dp))
Text(
text = commonState.lastUpdated(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()

View File

@@ -1359,8 +1359,8 @@ data class VaultItemState(
* Content data that is common for all item types.
*
* @property name The name of the item.
* @property lastUpdated A formatted date string indicating when the item was last
* updated.
* @param created A formatted string indicating when the item was created.
* @property lastUpdated A formatted string indicating when the item was last updated.
* @property notes Contains general notes taken by the user.
* @property customFields A list of custom fields that user has added.
* @property requiresCloneConfirmation Indicates user confirmation is required when
@@ -1377,7 +1377,8 @@ data class VaultItemState(
@Parcelize
data class Common(
val name: String,
val lastUpdated: String,
val created: Text,
val lastUpdated: Text,
val notes: String?,
val customFields: List<Custom>,
val requiresCloneConfirmation: Boolean,
@@ -1495,7 +1496,7 @@ data class VaultItemState(
val username: String?,
val passwordData: PasswordData?,
val uris: List<UriData>,
val passwordRevisionDate: String?,
val passwordRevisionDate: Text?,
val totpCodeItemData: TotpCodeItemData?,
val isPremiumUser: Boolean,
val canViewTotpCode: Boolean,

View File

@@ -1,34 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.item.component
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Update Text UI common for all item types.
*/
@Composable
fun VaultItemUpdateText(
header: String,
text: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.semantics(mergeDescendants = true) { },
) {
Text(
text = header,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
)
Text(
text = text,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
)
}
}

View File

@@ -61,10 +61,19 @@ fun CipherView.toViewState(
?.find { it.id == fieldView.hashCode().toString() },
)
},
lastUpdated = revisionDate.toFormattedDateTimeStyle(
dateStyle = FormatStyle.SHORT,
timeStyle = FormatStyle.SHORT,
clock = clock,
created = R.string.created.asText(
creationDate.toFormattedDateTimeStyle(
dateStyle = FormatStyle.MEDIUM,
timeStyle = FormatStyle.SHORT,
clock = clock,
),
),
lastUpdated = R.string.last_edited.asText(
revisionDate.toFormattedDateTimeStyle(
dateStyle = FormatStyle.MEDIUM,
timeStyle = FormatStyle.SHORT,
clock = clock,
),
),
notes = notes,
requiresCloneConfirmation = login?.fido2Credentials?.any() ?: false,
@@ -125,10 +134,11 @@ fun CipherView.toViewState(
passwordRevisionDate = loginValues
.passwordRevisionDate
?.toFormattedDateTimeStyle(
dateStyle = FormatStyle.SHORT,
dateStyle = FormatStyle.MEDIUM,
timeStyle = FormatStyle.SHORT,
clock = clock,
),
)
?.let { R.string.password_last_updated.asText(it) },
isPremiumUser = isPremiumUser,
canViewTotpCode = isPremiumUser || this.organizationUseTotp,
totpCodeItemData = totpCodeItemData,

View File

@@ -296,8 +296,9 @@ Scanning will happen automatically.</string>
<string name="autofill_and_save">Autofill and save</string>
<string name="organization">Organization</string>
<string name="try_again">Try again</string>
<string name="date_password_updated">Password updated</string>
<string name="date_updated">Updated</string>
<string name="created">Created: %1$s</string>
<string name="password_last_updated">Password last updated: %1$s</string>
<string name="last_edited">Last edited: %1$s</string>
<string name="invalid_email">Invalid email address.</string>
<string name="cards">Cards</string>
<string name="identities">Identities</string>

View File

@@ -7,7 +7,6 @@ import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.filter
import androidx.compose.ui.test.filterToOne
@@ -489,6 +488,28 @@ class VaultItemScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `created should be displayed according to state`() {
EMPTY_VIEW_STATES
.forEach { typeState ->
mutableStateFlow.update { it.copy(viewState = typeState) }
composeTestRule
.onNodeWithTextAfterScroll(text = "Created: Dec 1, 1969, 05:20 PM")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateCommonContent(currentState) {
copy(lastUpdated = R.string.created.asText("Feb 21, 1970, 1:30 AM"))
}
}
composeTestRule
.onNodeWithTextAfterScroll(text = "Created: Feb 21, 1970, 1:30 AM")
.assertIsDisplayed()
}
}
@Test
fun `lastUpdated should be displayed according to state`() {
EMPTY_VIEW_STATES
@@ -496,16 +517,18 @@ class VaultItemScreenTest : BitwardenComposeTest() {
mutableStateFlow.update { it.copy(viewState = typeState) }
composeTestRule
.onNodeWithTextAfterScroll("Updated: ")
.assertTextContains("12/31/69 06:16 PM")
.onNodeWithTextAfterScroll(text = "Last edited: Dec 31, 1969, 06:16 PM")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateCommonContent(currentState) { copy(lastUpdated = "12/31/69 06:20 PM") }
updateCommonContent(currentState) {
copy(lastUpdated = R.string.last_edited.asText("Dec 31, 1969, 06:20 PM"))
}
}
composeTestRule
.onNodeWithTextAfterScroll("Updated: ")
.assertTextContains("12/31/69 06:20 PM")
.onNodeWithTextAfterScroll(text = "Last edited: Dec 31, 1969, 06:20 PM")
.assertIsDisplayed()
}
}
@@ -2365,15 +2388,17 @@ class VaultItemScreenTest : BitwardenComposeTest() {
@Test
fun `in login state, password updated should be displayed according to state`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE) }
composeTestRule.onNodeWithTextAfterScroll("Password updated: ").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll("4/14/83 3:56 PM").assertIsDisplayed()
composeTestRule
.onNodeWithTextAfterScroll(text = "Password last updated: Apr 14, 1983 3:56 PM")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(passwordRevisionDate = null) }
}
composeTestRule.assertScrollableNodeDoesNotExist("Password updated: ")
composeTestRule.assertScrollableNodeDoesNotExist("4/14/83 3:56 PM")
composeTestRule.assertScrollableNodeDoesNotExist(
text = "Password last updated: Apr 14, 1983 3:56 PM",
)
}
//endregion login
@@ -2916,7 +2941,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
}
// First scroll past the security code field to avoid clicking the fab
composeTestRule.onNodeWithTextAfterScroll("Updated: ")
composeTestRule.onNodeWithTextAfterScroll("Last edited: Dec 31, 1969, 06:16 PM")
composeTestRule
.onNodeWithContentDescriptionAfterScroll("Copy security code")
.performClick()
@@ -3123,7 +3148,8 @@ private val DEFAULT_STATE: VaultItemState = VaultItemState(
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
VaultItemState.ViewState.Content.Common(
name = "cipher",
lastUpdated = "12/31/69 06:16 PM",
created = R.string.created.asText(""),
lastUpdated = R.string.last_edited.asText("Dec 31, 1969, 06:16 PM"),
notes = "Lots of notes",
customFields = listOf(
VaultItemState.ViewState.Content.Common.Custom.TextField(
@@ -3186,7 +3212,7 @@ private val DEFAULT_LOGIN: VaultItemState.ViewState.Content.ItemType.Login =
isLaunchable = true,
),
),
passwordRevisionDate = "4/14/83 3:56 PM",
passwordRevisionDate = R.string.password_last_updated.asText("Apr 14, 1983 3:56 PM"),
isPremiumUser = true,
totpCodeItemData = TotpCodeItemData(
periodSeconds = 30,
@@ -3239,7 +3265,8 @@ private val DEFAULT_SSH_KEY: VaultItemState.ViewState.Content.ItemType.SshKey =
private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
VaultItemState.ViewState.Content.Common(
name = "cipher",
lastUpdated = "12/31/69 06:16 PM",
created = R.string.created.asText("Dec 1, 1969, 05:20 PM"),
lastUpdated = R.string.last_edited.asText("Dec 31, 1969, 06:16 PM"),
notes = null,
customFields = emptyList(),
requiresCloneConfirmation = false,

View File

@@ -2515,7 +2515,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
isLaunchable = true,
),
),
passwordRevisionDate = "12/31/69 06:16 PM",
passwordRevisionDate = R.string.password_last_updated.asText("12/31/69 06:16 PM"),
isPremiumUser = true,
totpCodeItemData = TotpCodeItemData(
totpCode = "otpauth://totp/Example:alice@google.com" +
@@ -2569,7 +2569,8 @@ class VaultItemViewModelTest : BaseViewModelTest() {
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
VaultItemState.ViewState.Content.Common(
name = "login cipher",
lastUpdated = "12/31/69 06:16 PM",
created = R.string.created.asText("Dec 1, 1969, 05:20 PM"),
lastUpdated = R.string.last_edited.asText("Dec 31, 1969, 06:16 PM"),
notes = "Lots of notes",
customFields = listOf(
VaultItemState.ViewState.Content.Common.Custom.TextField(

View File

@@ -169,7 +169,8 @@ fun createCommonContent(
if (isEmpty) {
VaultItemState.ViewState.Content.Common(
name = "mockName",
lastUpdated = "1/1/70, 12:16 AM",
created = R.string.created.asText("Jan 1, 1970, 12:16 AM"),
lastUpdated = R.string.last_edited.asText("Jan 1, 1970, 12:16 AM"),
notes = null,
customFields = emptyList(),
requiresCloneConfirmation = false,
@@ -186,7 +187,8 @@ fun createCommonContent(
} else {
VaultItemState.ViewState.Content.Common(
name = "mockName",
lastUpdated = "1/1/70, 12:16 AM",
created = R.string.created.asText("Jan 1, 1970, 12:16 AM"),
lastUpdated = R.string.last_edited.asText("Jan 1, 1970, 12:16 AM"),
notes = "Lots of notes",
customFields = listOf(
FieldView(
@@ -267,7 +269,9 @@ fun createLoginContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemT
),
)
},
passwordRevisionDate = "1/1/70, 12:16 AM".takeUnless { isEmpty },
passwordRevisionDate = R.string.password_last_updated
.asText("Jan 1, 1970, 12:16 AM")
.takeUnless { isEmpty },
isPremiumUser = true,
totpCodeItemData = TotpCodeItemData(
periodSeconds = 30,