BIT-2342: Hide verification codes for items with password reprompt (#1353)

This commit is contained in:
Caleb Derosier 2024-05-09 16:31:37 -06:00 committed by Álison Fernandes
parent 179c5199e7
commit 903aa26876
10 changed files with 130 additions and 29 deletions

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.core.CipherRepromptType
import com.bitwarden.core.CipherView
import com.bitwarden.core.DateTime
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
@ -111,6 +112,10 @@ class TotpCodeManagerImpl(
id = cipherId,
name = cipher.name,
username = cipher.login?.username,
hasPasswordReprompt = when (cipher.reprompt) {
CipherRepromptType.PASSWORD -> true
CipherRepromptType.NONE -> false
},
)
}
.onFailure {

View file

@ -14,6 +14,7 @@ import com.bitwarden.core.LoginUriView
* @property id The cipher id of the item.
* @property name The name of the cipher item.
* @property username The username associated with the item.
* @property hasPasswordReprompt Indicates whether this item has a master password reprompt.
*/
data class VerificationCodeItem(
val code: String,
@ -25,4 +26,5 @@ data class VerificationCodeItem(
val id: String,
val name: String,
val username: String?,
val hasPasswordReprompt: Boolean,
)

View file

@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* The verification code item displayed to the user.
*
* @param authCode The code for the item.
* @param hideAuthCode Indicates whether the auth / verification code should be hidden.
* @param label The label for the item.
* @param periodSeconds The times span where the code is valid.
* @param timeLeftSeconds The seconds remaining until a new code is needed.
@ -45,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@Composable
fun VaultVerificationCodeItem(
authCode: String,
hideAuthCode: Boolean,
label: String,
periodSeconds: Int,
timeLeftSeconds: Int,
@ -103,21 +105,23 @@ fun VaultVerificationCodeItem(
periodSeconds = periodSeconds,
)
Text(
text = authCode.chunked(3).joinToString(" "),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
IconButton(
onClick = onCopyClick,
) {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_copy),
contentDescription = stringResource(id = R.string.copy),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp),
if (!hideAuthCode) {
Text(
text = authCode.chunked(3).joinToString(" "),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
IconButton(
onClick = onCopyClick,
) {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_copy),
contentDescription = stringResource(id = R.string.copy),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp),
)
}
}
}
}
@ -132,6 +136,7 @@ private fun VerificationCodeItem_preview() {
label = "Sample Label",
supportingLabel = "Supporting Label",
authCode = "1234567890".chunked(3).joinToString(" "),
hideAuthCode = false,
timeLeftSeconds = 15,
periodSeconds = 30,
onCopyClick = {},

View file

@ -179,6 +179,7 @@ private fun VerificationCodeContent(
timeLeftSeconds = it.timeLeftSeconds,
periodSeconds = it.periodSeconds,
authCode = it.authCode,
hideAuthCode = it.hideAuthCode,
onCopyClick = { onCopyClick(it.authCode) },
onItemClick = {
itemClick(it.id)

View file

@ -277,6 +277,7 @@ class VerificationCodeViewModel @Inject constructor(
VerificationCodeDisplayItem(
id = item.id,
authCode = item.code,
hideAuthCode = item.hasPasswordReprompt,
label = item.name,
supportingLabel = item.username,
periodSeconds = item.periodSeconds,
@ -381,6 +382,7 @@ data class VerificationCodeDisplayItem(
val timeLeftSeconds: Int,
val periodSeconds: Int,
val authCode: String,
val hideAuthCode: Boolean,
val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
) : Parcelable

View file

@ -41,6 +41,7 @@ fun createMockCipherView(
number: Int,
isDeleted: Boolean = false,
cipherType: CipherType = CipherType.LOGIN,
repromptType: CipherRepromptType = CipherRepromptType.NONE,
totp: String? = "mockTotp-$number",
folderId: String? = "mockId-$number",
clock: Clock = FIXED_CLOCK,
@ -75,7 +76,7 @@ fun createMockCipherView(
},
favorite = false,
passwordHistory = listOf(createMockPasswordHistoryView(number = number, clock)),
reprompt = CipherRepromptType.NONE,
reprompt = repromptType,
secureNote = createMockSecureNoteView().takeIf { cipherType == CipherType.SECURE_NOTE },
edit = false,
organizationUseTotp = false,

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.vault.manager
import app.cash.turbine.test
import com.bitwarden.core.CipherRepromptType
import com.bitwarden.core.TotpResponse
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
@ -94,16 +95,18 @@ class TotpCodeManagerTest {
}
@Test
fun `getTotpCodeStateFlow should have loaded item with a valid data passed in`() = runTest {
fun `getTotpCodeStateFlow should have loaded item with valid data passed in`() = runTest {
val totpResponse = TotpResponse("123456", 30u)
coEvery {
vaultSdkSource.generateTotp(any(), any(), any())
} returns totpResponse.asSuccess()
val cipherView = createMockCipherView(1)
val cipherView = createMockCipherView(
number = 1,
repromptType = CipherRepromptType.PASSWORD,
)
val expected = createVerificationCodeItem()
val expected = createVerificationCodeItem().copy(hasPasswordReprompt = true)
totpCodeManager.getTotpCodeStateFlow(userId, cipherView).test {
assertEquals(DataState.Loaded(expected), awaitItem())

View file

@ -129,6 +129,49 @@ class VerificationCodeScreenTest : BaseComposeTest() {
.assertIsDisplayed()
}
@Test
fun `auth code and copy button should be displayed according to state`() {
val authCode = "123 456"
mutableStateFlow.update {
DEFAULT_STATE.copy(
viewState = VerificationCodeState.ViewState.Content(
verificationCodeDisplayItems = listOf(
createDisplayItem(
number = 1,
hideAuthCode = false,
),
),
),
)
}
composeTestRule
.onNodeWithText(authCode)
.assertIsDisplayed()
composeTestRule
.onNodeWithContentDescription("Copy")
.assertIsDisplayed()
mutableStateFlow.update {
DEFAULT_STATE.copy(
viewState = VerificationCodeState.ViewState.Content(
verificationCodeDisplayItems = listOf(
createDisplayItem(
number = 1,
hideAuthCode = true,
),
),
),
)
}
composeTestRule
.onNodeWithText(authCode)
.assertIsNotDisplayed()
composeTestRule
.onNodeWithContentDescription("Copy")
.assertIsNotDisplayed()
}
@Test
fun `Items text should be displayed according to state`() {
val items = "Items"
@ -343,10 +386,14 @@ class VerificationCodeScreenTest : BaseComposeTest() {
}
}
private fun createDisplayItem(number: Int): VerificationCodeDisplayItem =
private fun createDisplayItem(
number: Int,
hideAuthCode: Boolean = false,
): VerificationCodeDisplayItem =
VerificationCodeDisplayItem(
id = number.toString(),
authCode = "123456",
hideAuthCode = hideAuthCode,
label = "Label $number",
supportingLabel = "Supporting Label $number",
periodSeconds = 30,

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.verificationcode
import android.net.Uri
import app.cash.turbine.test
import com.bitwarden.core.CipherRepromptType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@ -163,7 +164,10 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
mutableAuthCodeFlow.tryEmit(
value = DataState.Pending(
data = listOf(createVerificationCodeItem()),
data = listOf(
createVerificationCodeItem(number = 1),
createVerificationCodeItem(number = 2).copy(hasPasswordReprompt = true),
),
),
)
@ -205,7 +209,10 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
mutableAuthCodeFlow.tryEmit(
value = DataState.Error(
data = listOf(createVerificationCodeItem()),
data = listOf(
createVerificationCodeItem(number = 1),
createVerificationCodeItem(number = 2).copy(hasPasswordReprompt = true),
),
error = IllegalStateException(),
),
)
@ -317,7 +324,10 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
mutableAuthCodeFlow.tryEmit(
value = DataState.NoNetwork(
listOf(createVerificationCodeItem()),
data = listOf(
createVerificationCodeItem(number = 1),
createVerificationCodeItem(number = 2).copy(hasPasswordReprompt = true),
),
),
)
@ -358,7 +368,10 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
mutableAuthCodeFlow.tryEmit(
value = DataState.Loaded(
listOf(createVerificationCodeItem()),
data = listOf(
createVerificationCodeItem(number = 1),
createVerificationCodeItem(number = 2).copy(hasPasswordReprompt = true),
),
),
)
@ -461,6 +474,27 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
VerificationCodeDisplayItem(
id = cipherView.id.toString(),
authCode = "123456",
hideAuthCode = false,
label = cipherView.name,
supportingLabel = cipherView.login?.username,
periodSeconds = 30,
timeLeftSeconds = 30,
startIcon = cipherView.login?.uris.toLoginIconData(
isIconLoadingDisabled = initialState.isIconLoadingDisabled,
baseIconUrl = initialState.baseIconUrl,
),
)
},
createMockCipherView(
number = 2,
isDeleted = false,
repromptType = CipherRepromptType.PASSWORD,
)
.let { cipherView ->
VerificationCodeDisplayItem(
id = cipherView.id.toString(),
authCode = "123456",
hideAuthCode = true,
label = cipherView.name,
supportingLabel = cipherView.login?.username,
periodSeconds = 30,

View file

@ -3,15 +3,16 @@ package com.x8bit.bitwarden.ui.vault.feature.verificationcode.util
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
fun createVerificationCodeItem() =
fun createVerificationCodeItem(number: Int = 1) =
VerificationCodeItem(
code = "123456",
totpCode = "mockTotp-1",
totpCode = "mockTotp-$number",
periodSeconds = 30,
id = "mockId-1",
id = "mockId-$number",
issueTime = 1698408000000,
timeLeftSeconds = 30,
name = "mockName-1",
name = "mockName-$number",
uriLoginViewList = createMockLoginView(1).uris,
username = "mockUsername-1",
username = "mockUsername-$number",
hasPasswordReprompt = false,
)