This commit is contained in:
Philip Cappelli
2025-03-27 17:32:53 -04:00
parent c0707ae08c
commit c51f61c585
6 changed files with 53 additions and 37 deletions

View File

@@ -50,6 +50,7 @@ private const val AUTH_CODE_SPACING_INTERVAL = 3
fun VaultItemLoginContent(
commonState: VaultItemState.ViewState.Content.Common,
loginItemState: VaultItemState.ViewState.Content.ItemType.Login,
totpCodeItemData: TotpCodeItemData?,
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
modifier: Modifier = Modifier,
@@ -136,7 +137,7 @@ fun VaultItemLoginContent(
}
}
loginItemState.totpCodeItemData?.let { totpCodeItemData ->
totpCodeItemData?.let { totpCodeItemData ->
item(key = "totpCode") {
Spacer(modifier = Modifier.height(8.dp))
TotpField(
@@ -422,7 +423,7 @@ private fun PasswordField(
private fun TotpField(
totpCodeItemData: TotpCodeItemData,
enabled: Boolean,
onCopyTotpClick: () -> Unit,
onCopyTotpClick: (String) -> Unit,
onAuthenticatorHelpToolTipClick: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -448,7 +449,7 @@ private fun TotpField(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_totp),
onClick = onCopyTotpClick,
onClick = { onCopyTotpClick(totpCodeItemData.totpCode) },
modifier = Modifier.testTag(tag = "LoginCopyTotpButton"),
)
},

View File

@@ -45,6 +45,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHan
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
/**
@@ -260,6 +261,7 @@ fun VaultItemScreen(
) {
VaultItemContent(
viewState = state.viewState,
totpCodeItemData = state.totpCodeItemData,
modifier = Modifier
.fillMaxSize(),
vaultCommonItemTypeHandlers = remember(viewModel) {
@@ -350,6 +352,7 @@ private fun VaultItemDialogs(
@Composable
private fun VaultItemContent(
viewState: VaultItemState.ViewState,
totpCodeItemData: TotpCodeItemData?,
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
@@ -370,6 +373,7 @@ private fun VaultItemContent(
VaultItemLoginContent(
commonState = viewState.common,
loginItemState = viewState.type,
totpCodeItemData = totpCodeItemData,
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
vaultLoginItemTypeHandlers = vaultLoginItemTypeHandlers,
modifier = modifier,

View File

@@ -104,23 +104,14 @@ class VaultItemViewModel @Inject constructor(
vaultRepository.getAuthCodeFlow(state.vaultItemId),
vaultRepository.collectionsStateFlow,
vaultRepository.foldersStateFlow,
) { cipherViewState, userState, authCodeState, collectionsState, folderState ->
val totpCodeData = authCodeState.data?.let {
TotpCodeItemData(
periodSeconds = it.periodSeconds,
timeLeftSeconds = it.timeLeftSeconds,
totpCode = it.totpCode,
verificationCode = it.code,
)
}
) { cipherViewState, userState, collectionsState, folderState ->
VaultItemAction.Internal.VaultDataReceive(
userState = userState,
vaultDataState = combineDataStates(
cipherViewState,
authCodeState,
collectionsState,
folderState,
) { _, _, _, _ ->
) { _, _, _ ->
// We are only combining the DataStates to know the overall state,
// we map it to the appropriate value below.
}
@@ -165,7 +156,6 @@ class VaultItemViewModel @Inject constructor(
VaultItemStateData(
cipher = cipherView,
totpCodeItemData = totpCodeData,
canDelete = canDelete,
canAssociateToCollections = canAssignToCollections,
canEdit = canEdit,
@@ -181,6 +171,21 @@ class VaultItemViewModel @Inject constructor(
.map { VaultItemAction.Internal.IsIconLoadingDisabledUpdateReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
vaultRepository.getAuthCodeFlow(state.vaultItemId)
.map {
VaultItemAction.Internal.TotpDataReceive(
totpData = TotpCodeItemData(
periodSeconds = it.data?.periodSeconds ?: 0,
timeLeftSeconds = it.data?.timeLeftSeconds ?: 0,
totpCode = it.data?.totpCode ?: "",
verificationCode = it.data?.code ?: "",
)
)
}
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: VaultItemAction) {
@@ -602,7 +607,7 @@ class VaultItemViewModel @Inject constructor(
}
is VaultItemAction.ItemType.Login.CopyTotpClick -> {
handleCopyTotpClick()
handleCopyTotpClick(action)
}
is VaultItemAction.ItemType.Login.CopyUriClick -> {
@@ -663,14 +668,11 @@ class VaultItemViewModel @Inject constructor(
}
}
private fun handleCopyTotpClick() {
onLoginContent { _, login ->
val code = login.totpCodeItemData?.verificationCode ?: return@onLoginContent
clipboardManager.setText(
text = code,
toastDescriptorOverride = R.string.totp.asText(),
)
}
private fun handleCopyTotpClick(action: VaultItemAction.ItemType.Login.CopyTotpClick) {
clipboardManager.setText(
text = action.code,
toastDescriptorOverride = R.string.totp.asText(),
)
}
private fun handleCopyUriClick(action: VaultItemAction.ItemType.Login.CopyUriClick) {
@@ -1093,6 +1095,10 @@ class VaultItemViewModel @Inject constructor(
is VaultItemAction.Internal.IsIconLoadingDisabledUpdateReceive -> {
handleIsIconLoadingDisabledUpdateReceive(action)
}
is VaultItemAction.Internal.TotpDataReceive -> {
handleTotpDataReceive(action)
}
}
}
@@ -1198,7 +1204,6 @@ class VaultItemViewModel @Inject constructor(
previousState = state.viewState.asContentOrNull(),
isPremiumUser = account.isPremium,
hasMasterPassword = account.hasMasterPassword,
totpCodeItemData = this.data?.totpCodeItemData,
canDelete = this.data?.canDelete == true,
canAssignToCollections = this.data?.canAssociateToCollections == true,
canEdit = this.data?.canEdit == true,
@@ -1353,6 +1358,12 @@ class VaultItemViewModel @Inject constructor(
mutableStateFlow.update { it.copy(isIconLoadingDisabled = action.isDisabled) }
}
private fun handleTotpDataReceive(
action: VaultItemAction.Internal.TotpDataReceive,
) {
mutableStateFlow.update { it.copy(totpCodeItemData = action.totpData) }
}
//endregion Internal Type Handlers
private fun updateDialogState(dialog: VaultItemState.DialogState?) {
@@ -1440,6 +1451,7 @@ data class VaultItemState(
val vaultItemId: String,
val cipherType: VaultItemCipherType,
val viewState: ViewState,
val totpCodeItemData: TotpCodeItemData?,
val dialog: DialogState?,
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
@@ -1653,7 +1665,6 @@ data class VaultItemState(
* @property uris The URI associated with the login item.
* @property passwordRevisionDate An optional string indicating the last time the
* password was changed.
* @property totpCodeItemData The optional data related the TOTP code.
* @property isPremiumUser Indicates if the user has subscribed to a premium
* account.
* @property canViewTotpCode Indicates if the user can view an associated TOTP code.
@@ -1662,7 +1673,7 @@ data class VaultItemState(
*
* **NOTE** [canViewTotpCode] currently supports a deprecated edge case where an
* organization supports TOTP but not through the current premium model.
* This additional field is added to allow for [isPremiumUser] to be an independent
* This additional field is added to allow for [] to be an independent
* value.
* @see [CipherView.organizationUseTotp]
*
@@ -1673,7 +1684,6 @@ data class VaultItemState(
val passwordData: PasswordData?,
val uris: List<UriData>,
val passwordRevisionDate: String?,
val totpCodeItemData: TotpCodeItemData?,
val isPremiumUser: Boolean,
val canViewTotpCode: Boolean,
val fido2CredentialCreationDateText: Text?,
@@ -1685,8 +1695,7 @@ data class VaultItemState(
val hasLoginCredentials: Boolean
get() = username != null ||
passwordData != null ||
fido2CredentialCreationDateText != null ||
totpCodeItemData != null
fido2CredentialCreationDateText != null
/**
* A wrapper for the password data.
@@ -2127,7 +2136,9 @@ sealed class VaultItemAction {
/**
* The user has clicked the copy button for the TOTP code.
*/
data object CopyTotpClick : Login()
data class CopyTotpClick(
val code: String,
) : Login()
/**
* The user has clicked the copy button for a URI.
@@ -2285,6 +2296,10 @@ sealed class VaultItemAction {
val vaultDataState: DataState<VaultItemStateData>,
) : Internal()
data class TotpDataReceive(
val totpData: TotpCodeItemData?,
) : Internal()
/**
* Indicates that the verify password result has been received.
*/

View File

@@ -11,7 +11,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
data class VaultLoginItemTypeHandlers(
val onCheckForBreachClick: () -> Unit,
val onCopyPasswordClick: () -> Unit,
val onCopyTotpCodeClick: () -> Unit,
val onCopyTotpCodeClick: (String) -> Unit,
val onAuthenticatorHelpToolTipClick: () -> Unit,
val onCopyUriClick: (String) -> Unit,
val onCopyUsernameClick: () -> Unit,
@@ -35,7 +35,7 @@ data class VaultLoginItemTypeHandlers(
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
},
onCopyTotpCodeClick = {
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyTotpClick)
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyTotpClick(it))
},
onAuthenticatorHelpToolTipClick = {
viewModel.trySendAction(

View File

@@ -7,7 +7,6 @@ import kotlinx.collections.immutable.ImmutableList
* The state containing totp code item information and the cipher for the item.
*
* @property cipher The cipher view for the item.
* @property totpCodeItemData The data for the totp code.
* @property canDelete Whether the item can be deleted.
* @property canAssociateToCollections Whether the item can be associated to a collection.
* @property canEdit Whether the item can be edited.
@@ -15,7 +14,6 @@ import kotlinx.collections.immutable.ImmutableList
*/
data class VaultItemStateData(
val cipher: CipherView?,
val totpCodeItemData: TotpCodeItemData?,
val canDelete: Boolean,
val canAssociateToCollections: Boolean,
val canEdit: Boolean,

View File

@@ -42,7 +42,6 @@ fun CipherView.toViewState(
previousState: VaultItemState.ViewState.Content?,
isPremiumUser: Boolean,
hasMasterPassword: Boolean,
totpCodeItemData: TotpCodeItemData?,
clock: Clock = Clock.systemDefaultZone(),
canDelete: Boolean,
canAssignToCollections: Boolean,
@@ -125,7 +124,6 @@ fun CipherView.toViewState(
),
isPremiumUser = isPremiumUser,
canViewTotpCode = isPremiumUser || this.organizationUseTotp,
totpCodeItemData = totpCodeItemData,
fido2CredentialCreationDateText = loginValues
.fido2Credentials
?.firstOrNull()