mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 23:25:45 +03:00
BIT-526: Clone vault item (#713)
This commit is contained in:
parent
3ec95b0ffd
commit
a760127711
13 changed files with 235 additions and 30 deletions
|
@ -92,8 +92,15 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
)
|
)
|
||||||
vaultItemDestination(
|
vaultItemDestination(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onNavigateToVaultEditItem = {
|
onNavigateToVaultEditItem = { vaultItemId, isClone ->
|
||||||
navController.navigateToVaultAddEdit(VaultAddEditType.EditItem(it))
|
navController.navigateToVaultAddEdit(
|
||||||
|
if (isClone) {
|
||||||
|
VaultAddEditType.CloneItem(vaultItemId)
|
||||||
|
} else {
|
||||||
|
VaultAddEditType.EditItem(vaultItemId)
|
||||||
|
},
|
||||||
|
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onNavigateToMoveToOrganization = {
|
onNavigateToMoveToOrganization = {
|
||||||
navController.navigateToVaultMoveToOrganization(it)
|
navController.navigateToVaultMoveToOrganization(it)
|
||||||
|
|
|
@ -3,11 +3,14 @@ package com.x8bit.bitwarden.ui.platform.manager.di
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManagerImpl
|
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManagerImpl
|
||||||
|
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
|
||||||
|
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManagerImpl
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides UI-based managers in the platform package.
|
* Provides UI-based managers in the platform package.
|
||||||
|
@ -15,11 +18,18 @@ import dagger.hilt.components.SingletonComponent
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class PlatformUiManagerModule {
|
class PlatformUiManagerModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
@Singleton
|
||||||
fun provideIntentManager(
|
fun provideIntentManager(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
): IntentManager =
|
): IntentManager =
|
||||||
IntentManagerImpl(
|
IntentManagerImpl(
|
||||||
context = context,
|
context = context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideResourceManager(@ApplicationContext context: Context): ResourceManager =
|
||||||
|
ResourceManagerImpl(context = context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.manager.resource
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for managing resources.
|
||||||
|
*/
|
||||||
|
interface ResourceManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for returning a permission string from a [resId].
|
||||||
|
*/
|
||||||
|
fun getString(@StringRes resId: Int): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for returning a permission string from a [resId] with [formatArgs].
|
||||||
|
*/
|
||||||
|
fun getString(@StringRes resId: Int, vararg formatArgs: Any): String
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.manager.resource
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary implementation of [ResourceManager].
|
||||||
|
*/
|
||||||
|
@OmitFromCoverage
|
||||||
|
class ResourceManagerImpl(private val context: Context) : ResourceManager {
|
||||||
|
override fun getString(@StringRes resId: Int): String =
|
||||||
|
context.getString(resId)
|
||||||
|
|
||||||
|
override fun getString(@StringRes resId: Int, vararg formatArgs: Any): String =
|
||||||
|
context.getString(resId, *formatArgs)
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||||
|
|
||||||
private const val ADD_TYPE: String = "add"
|
private const val ADD_TYPE: String = "add"
|
||||||
private const val EDIT_TYPE: String = "edit"
|
private const val EDIT_TYPE: String = "edit"
|
||||||
|
private const val CLONE_TYPE: String = "clone"
|
||||||
private const val EDIT_ITEM_ID: String = "vault_edit_id"
|
private const val EDIT_ITEM_ID: String = "vault_edit_id"
|
||||||
|
|
||||||
private const val ADD_EDIT_ITEM_PREFIX: String = "vault_add_edit_item"
|
private const val ADD_EDIT_ITEM_PREFIX: String = "vault_add_edit_item"
|
||||||
|
@ -32,6 +33,7 @@ data class VaultAddEditArgs(
|
||||||
vaultAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) {
|
vaultAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) {
|
||||||
ADD_TYPE -> VaultAddEditType.AddItem
|
ADD_TYPE -> VaultAddEditType.AddItem
|
||||||
EDIT_TYPE -> VaultAddEditType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
|
EDIT_TYPE -> VaultAddEditType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
|
||||||
|
CLONE_TYPE -> VaultAddEditType.CloneItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
|
||||||
else -> throw IllegalStateException("Unknown VaultAddEditType.")
|
else -> throw IllegalStateException("Unknown VaultAddEditType.")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -79,7 +81,12 @@ private fun VaultAddEditType.toTypeString(): String =
|
||||||
when (this) {
|
when (this) {
|
||||||
is VaultAddEditType.AddItem -> ADD_TYPE
|
is VaultAddEditType.AddItem -> ADD_TYPE
|
||||||
is VaultAddEditType.EditItem -> EDIT_TYPE
|
is VaultAddEditType.EditItem -> EDIT_TYPE
|
||||||
|
is VaultAddEditType.CloneItem -> CLONE_TYPE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun VaultAddEditType.toIdOrNull(): String? =
|
private fun VaultAddEditType.toIdOrNull(): String? =
|
||||||
(this as? VaultAddEditType.EditItem)?.vaultItemId
|
when (this) {
|
||||||
|
is VaultAddEditType.AddItem -> null
|
||||||
|
is VaultAddEditType.CloneItem -> vaultItemId
|
||||||
|
is VaultAddEditType.EditItem -> vaultItemId
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||||
|
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
|
||||||
|
@ -56,6 +57,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
private val clipboardManager: BitwardenClipboardManager,
|
private val clipboardManager: BitwardenClipboardManager,
|
||||||
private val vaultRepository: VaultRepository,
|
private val vaultRepository: VaultRepository,
|
||||||
private val generatorRepository: GeneratorRepository,
|
private val generatorRepository: GeneratorRepository,
|
||||||
|
private val resourceManager: ResourceManager,
|
||||||
) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>(
|
) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>(
|
||||||
// We load the state from the savedStateHandle for testing purposes.
|
// We load the state from the savedStateHandle for testing purposes.
|
||||||
initialState = savedStateHandle[KEY_STATE]
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
|
@ -70,6 +72,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
is VaultAddEditType.EditItem -> VaultAddEditState.ViewState.Loading
|
is VaultAddEditType.EditItem -> VaultAddEditState.ViewState.Loading
|
||||||
|
is VaultAddEditType.CloneItem -> VaultAddEditState.ViewState.Loading
|
||||||
},
|
},
|
||||||
dialog = null,
|
dialog = null,
|
||||||
)
|
)
|
||||||
|
@ -79,18 +82,18 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
//region Initialization and Overrides
|
//region Initialization and Overrides
|
||||||
|
|
||||||
init {
|
init {
|
||||||
when (val vaultAddEditType = state.vaultAddEditType) {
|
state
|
||||||
VaultAddEditType.AddItem -> Unit
|
.vaultAddEditType
|
||||||
is VaultAddEditType.EditItem -> {
|
.vaultItemId
|
||||||
|
?.let { itemId ->
|
||||||
vaultRepository
|
vaultRepository
|
||||||
.getVaultItemStateFlow(vaultAddEditType.vaultItemId)
|
.getVaultItemStateFlow(itemId)
|
||||||
// We'll stop getting updates as soon as we get some loaded data.
|
// We'll stop getting updates as soon as we get some loaded data.
|
||||||
.takeUntilLoaded()
|
.takeUntilLoaded()
|
||||||
.map { VaultAddEditAction.Internal.VaultDataReceive(it) }
|
.map { VaultAddEditAction.Internal.VaultDataReceive(it) }
|
||||||
.onEach(::sendAction)
|
.onEach(::sendAction)
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
vaultRepository
|
vaultRepository
|
||||||
.totpCodeFlow
|
.totpCodeFlow
|
||||||
|
@ -240,6 +243,11 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
sendAction(VaultAddEditAction.Internal.UpdateCipherResultReceive(result))
|
sendAction(VaultAddEditAction.Internal.UpdateCipherResultReceive(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultAddEditType.CloneItem -> {
|
||||||
|
val result = vaultRepository.createCipher(cipherView = content.toCipherView())
|
||||||
|
sendAction(VaultAddEditAction.Internal.CreateCipherResultReceive(result))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -810,6 +818,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
private fun handleVaultDataReceive(action: VaultAddEditAction.Internal.VaultDataReceive) {
|
private fun handleVaultDataReceive(action: VaultAddEditAction.Internal.VaultDataReceive) {
|
||||||
when (val vaultDataState = action.vaultDataState) {
|
when (val vaultDataState = action.vaultDataState) {
|
||||||
is DataState.Error -> {
|
is DataState.Error -> {
|
||||||
|
@ -827,7 +836,10 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
it.copy(
|
it.copy(
|
||||||
viewState = vaultDataState
|
viewState = vaultDataState
|
||||||
.data
|
.data
|
||||||
?.toViewState()
|
?.toViewState(
|
||||||
|
isClone = it.isCloneMode,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
?: VaultAddEditState.ViewState.Error(
|
?: VaultAddEditState.ViewState.Error(
|
||||||
message = R.string.generic_error_message.asText(),
|
message = R.string.generic_error_message.asText(),
|
||||||
),
|
),
|
||||||
|
@ -858,7 +870,10 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
it.copy(
|
it.copy(
|
||||||
viewState = vaultDataState
|
viewState = vaultDataState
|
||||||
.data
|
.data
|
||||||
?.toViewState()
|
?.toViewState(
|
||||||
|
isClone = it.isCloneMode,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
?: VaultAddEditState.ViewState.Error(
|
?: VaultAddEditState.ViewState.Error(
|
||||||
message = R.string.generic_error_message.asText(),
|
message = R.string.generic_error_message.asText(),
|
||||||
),
|
),
|
||||||
|
@ -1041,6 +1056,7 @@ data class VaultAddEditState(
|
||||||
get() = when (vaultAddEditType) {
|
get() = when (vaultAddEditType) {
|
||||||
VaultAddEditType.AddItem -> R.string.add_item.asText()
|
VaultAddEditType.AddItem -> R.string.add_item.asText()
|
||||||
is VaultAddEditType.EditItem -> R.string.edit_item.asText()
|
is VaultAddEditType.EditItem -> R.string.edit_item.asText()
|
||||||
|
is VaultAddEditType.CloneItem -> R.string.add_item.asText()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1048,6 +1064,11 @@ data class VaultAddEditState(
|
||||||
*/
|
*/
|
||||||
val isAddItemMode: Boolean get() = vaultAddEditType == VaultAddEditType.AddItem
|
val isAddItemMode: Boolean get() = vaultAddEditType == VaultAddEditType.AddItem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to determine if the UI should display the content in clone mode.
|
||||||
|
*/
|
||||||
|
val isCloneMode: Boolean get() = vaultAddEditType is VaultAddEditType.CloneItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum representing the main type options for the vault, such as LOGIN, CARD, etc.
|
* Enum representing the main type options for the vault, such as LOGIN, CARD, etc.
|
||||||
*
|
*
|
||||||
|
|
|
@ -7,6 +7,7 @@ import com.bitwarden.core.FieldType
|
||||||
import com.bitwarden.core.FieldView
|
import com.bitwarden.core.FieldView
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
||||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
|
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
|
||||||
|
@ -18,7 +19,10 @@ import java.util.UUID
|
||||||
/**
|
/**
|
||||||
* Transforms [CipherView] into [VaultAddEditState.ViewState].
|
* Transforms [CipherView] into [VaultAddEditState.ViewState].
|
||||||
*/
|
*/
|
||||||
fun CipherView.toViewState(): VaultAddEditState.ViewState =
|
fun CipherView.toViewState(
|
||||||
|
isClone: Boolean,
|
||||||
|
resourceManager: ResourceManager,
|
||||||
|
): VaultAddEditState.ViewState =
|
||||||
VaultAddEditState.ViewState.Content(
|
VaultAddEditState.ViewState.Content(
|
||||||
type = when (type) {
|
type = when (type) {
|
||||||
CipherType.LOGIN -> {
|
CipherType.LOGIN -> {
|
||||||
|
@ -63,7 +67,10 @@ fun CipherView.toViewState(): VaultAddEditState.ViewState =
|
||||||
},
|
},
|
||||||
common = VaultAddEditState.ViewState.Content.Common(
|
common = VaultAddEditState.ViewState.Content.Common(
|
||||||
originalCipher = this,
|
originalCipher = this,
|
||||||
name = this.name,
|
name = name.appendCloneTextIfRequired(
|
||||||
|
isClone = isClone,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
),
|
||||||
favorite = this.favorite,
|
favorite = this.favorite,
|
||||||
masterPasswordReprompt = this.reprompt == CipherRepromptType.PASSWORD,
|
masterPasswordReprompt = this.reprompt == CipherRepromptType.PASSWORD,
|
||||||
notes = this.notes.orEmpty(),
|
notes = this.notes.orEmpty(),
|
||||||
|
@ -121,3 +128,13 @@ private fun String?.toExpirationMonthOrDefault(): VaultCardExpirationMonth =
|
||||||
.entries
|
.entries
|
||||||
.find { it.number == this }
|
.find { it.number == this }
|
||||||
?: VaultCardExpirationMonth.SELECT
|
?: VaultCardExpirationMonth.SELECT
|
||||||
|
|
||||||
|
private fun String.appendCloneTextIfRequired(
|
||||||
|
isClone: Boolean,
|
||||||
|
resourceManager: ResourceManager,
|
||||||
|
): String =
|
||||||
|
if (isClone) {
|
||||||
|
plus(" - ${resourceManager.getString(R.string.clone)}")
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ data class VaultItemArgs(val vaultItemId: String) {
|
||||||
*/
|
*/
|
||||||
fun NavGraphBuilder.vaultItemDestination(
|
fun NavGraphBuilder.vaultItemDestination(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
|
onNavigateToVaultEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
|
||||||
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
|
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions(
|
composableWithSlideTransitions(
|
||||||
|
|
|
@ -59,7 +59,7 @@ fun VaultItemScreen(
|
||||||
viewModel: VaultItemViewModel = hiltViewModel(),
|
viewModel: VaultItemViewModel = hiltViewModel(),
|
||||||
intentManager: IntentManager = LocalIntentManager.current,
|
intentManager: IntentManager = LocalIntentManager.current,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToVaultAddEditItem: (vaultItemId: String) -> Unit,
|
onNavigateToVaultAddEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
|
||||||
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
|
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
@ -75,8 +75,7 @@ fun VaultItemScreen(
|
||||||
VaultItemEvent.NavigateBack -> onNavigateBack()
|
VaultItemEvent.NavigateBack -> onNavigateBack()
|
||||||
|
|
||||||
is VaultItemEvent.NavigateToAddEdit -> {
|
is VaultItemEvent.NavigateToAddEdit -> {
|
||||||
// TODO Implement cloning in BIT-526
|
onNavigateToVaultAddEditItem(event.itemId, event.isClone)
|
||||||
onNavigateToVaultAddEditItem(event.itemId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is VaultItemEvent.NavigateToPasswordHistory -> {
|
is VaultItemEvent.NavigateToPasswordHistory -> {
|
||||||
|
|
|
@ -7,19 +7,34 @@ import kotlinx.parcelize.Parcelize
|
||||||
* Represents the difference between create a completely new cipher and editing an existing one.
|
* Represents the difference between create a completely new cipher and editing an existing one.
|
||||||
*/
|
*/
|
||||||
sealed class VaultAddEditType : Parcelable {
|
sealed class VaultAddEditType : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the vault item (nullable).
|
||||||
|
*/
|
||||||
|
abstract val vaultItemId: String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that we want to create a completely new vault item.
|
* Indicates that we want to create a completely new vault item.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object AddItem : VaultAddEditType()
|
data object AddItem : VaultAddEditType() {
|
||||||
|
override val vaultItemId: String?
|
||||||
|
get() = null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that we want to edit an existing item.
|
* Indicates that we want to edit an existing item.
|
||||||
*
|
|
||||||
* @param vaultItemId The ID of the vault item to edit.
|
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class EditItem(
|
data class EditItem(
|
||||||
val vaultItemId: String,
|
override val vaultItemId: String,
|
||||||
|
) : VaultAddEditType()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that we want to clone an existing item.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class CloneItem(
|
||||||
|
override val vaultItemId: String,
|
||||||
) : VaultAddEditType()
|
) : VaultAddEditType()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
|
||||||
|
@ -58,7 +59,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
private val totpTestCodeFlow: MutableSharedFlow<TotpCodeResult> = bufferedMutableSharedFlow()
|
private val totpTestCodeFlow: MutableSharedFlow<TotpCodeResult> = bufferedMutableSharedFlow()
|
||||||
|
|
||||||
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
|
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
|
||||||
|
private val resourceManager: ResourceManager = mockk()
|
||||||
private val clipboardManager: BitwardenClipboardManager = mockk()
|
private val clipboardManager: BitwardenClipboardManager = mockk()
|
||||||
private val vaultRepository: VaultRepository = mockk {
|
private val vaultRepository: VaultRepository = mockk {
|
||||||
every { getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableVaultItemFlow
|
every { getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableVaultItemFlow
|
||||||
|
@ -131,6 +132,25 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial clone state should be correct`() = runTest {
|
||||||
|
val vaultAddEditType = VaultAddEditType.CloneItem(DEFAULT_EDIT_ITEM_ID)
|
||||||
|
val initState = createVaultAddItemState(vaultAddEditType = vaultAddEditType)
|
||||||
|
val viewModel = createAddVaultItemViewModel(
|
||||||
|
savedStateHandle = createSavedStateHandleWithState(
|
||||||
|
state = initState,
|
||||||
|
vaultAddEditType = vaultAddEditType,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
initState.copy(viewState = VaultAddEditState.ViewState.Loading),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
vaultRepository.getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CloseClick should emit NavigateBack`() = runTest {
|
fun `CloseClick should emit NavigateBack`() = runTest {
|
||||||
val viewModel = createAddVaultItemViewModel()
|
val viewModel = createAddVaultItemViewModel()
|
||||||
|
@ -254,7 +274,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
name = "mockName",
|
name = "mockName",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
every { cipherView.toViewState() } returns stateWithName.viewState
|
every {
|
||||||
|
cipherView.toViewState(
|
||||||
|
isClone = false,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
|
} returns stateWithName.viewState
|
||||||
mutableVaultItemFlow.value = DataState.Loaded(cipherView)
|
mutableVaultItemFlow.value = DataState.Loaded(cipherView)
|
||||||
|
|
||||||
val viewModel = createAddVaultItemViewModel(
|
val viewModel = createAddVaultItemViewModel(
|
||||||
|
@ -276,7 +301,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
coVerify(exactly = 1) {
|
coVerify(exactly = 1) {
|
||||||
cipherView.toViewState()
|
cipherView.toViewState(
|
||||||
|
isClone = false,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
|
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -294,7 +322,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
every { cipherView.toViewState() } returns stateWithName.viewState
|
every {
|
||||||
|
cipherView.toViewState(
|
||||||
|
isClone = false,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
|
} returns stateWithName.viewState
|
||||||
coEvery {
|
coEvery {
|
||||||
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
|
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
|
||||||
} returns UpdateCipherResult.Error(errorMessage = null)
|
} returns UpdateCipherResult.Error(errorMessage = null)
|
||||||
|
@ -336,7 +369,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
val errorMessage = "You do not have permission to edit this."
|
val errorMessage = "You do not have permission to edit this."
|
||||||
|
|
||||||
every { cipherView.toViewState() } returns stateWithName.viewState
|
every {
|
||||||
|
cipherView.toViewState(
|
||||||
|
isClone = false,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
|
} returns stateWithName.viewState
|
||||||
coEvery {
|
coEvery {
|
||||||
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
|
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
|
||||||
} returns UpdateCipherResult.Error(errorMessage = errorMessage)
|
} returns UpdateCipherResult.Error(errorMessage = errorMessage)
|
||||||
|
@ -1192,6 +1230,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
clipboardManager = clipboardManager,
|
clipboardManager = clipboardManager,
|
||||||
vaultRepository = vaultRepository,
|
vaultRepository = vaultRepository,
|
||||||
generatorRepository = generatorRepository,
|
generatorRepository = generatorRepository,
|
||||||
|
resourceManager = resourceManager,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1484,6 +1523,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
when (vaultAddEditType) {
|
when (vaultAddEditType) {
|
||||||
VaultAddEditType.AddItem -> "add"
|
VaultAddEditType.AddItem -> "add"
|
||||||
is VaultAddEditType.EditItem -> "edit"
|
is VaultAddEditType.EditItem -> "edit"
|
||||||
|
is VaultAddEditType.CloneItem -> "clone"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
set("vault_edit_id", (vaultAddEditType as? VaultAddEditType.EditItem)?.vaultItemId)
|
set("vault_edit_id", (vaultAddEditType as? VaultAddEditType.EditItem)?.vaultItemId)
|
||||||
|
@ -1494,12 +1534,14 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager,
|
bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager,
|
||||||
vaultRepo: VaultRepository = vaultRepository,
|
vaultRepo: VaultRepository = vaultRepository,
|
||||||
generatorRepo: GeneratorRepository = generatorRepository,
|
generatorRepo: GeneratorRepository = generatorRepository,
|
||||||
|
bitwardenResourceManager: ResourceManager = resourceManager,
|
||||||
): VaultAddEditViewModel =
|
): VaultAddEditViewModel =
|
||||||
VaultAddEditViewModel(
|
VaultAddEditViewModel(
|
||||||
savedStateHandle = savedStateHandle,
|
savedStateHandle = savedStateHandle,
|
||||||
clipboardManager = bitwardenClipboardManager,
|
clipboardManager = bitwardenClipboardManager,
|
||||||
vaultRepository = vaultRepo,
|
vaultRepository = vaultRepo,
|
||||||
generatorRepository = generatorRepo,
|
generatorRepository = generatorRepo,
|
||||||
|
resourceManager = bitwardenResourceManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,10 +14,12 @@ import com.bitwarden.core.SecureNoteType
|
||||||
import com.bitwarden.core.SecureNoteView
|
import com.bitwarden.core.SecureNoteView
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
||||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.unmockkStatic
|
import io.mockk.unmockkStatic
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
@ -29,6 +31,10 @@ import java.util.UUID
|
||||||
|
|
||||||
class CipherViewExtensionsTest {
|
class CipherViewExtensionsTest {
|
||||||
|
|
||||||
|
private val resourceManager: ResourceManager = mockk {
|
||||||
|
every { getString(R.string.clone) } returns "Clone"
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
mockkStatic(UUID::randomUUID)
|
mockkStatic(UUID::randomUUID)
|
||||||
|
@ -44,7 +50,10 @@ class CipherViewExtensionsTest {
|
||||||
fun `toViewState should create a Card ViewState`() {
|
fun `toViewState should create a Card ViewState`() {
|
||||||
val cipherView = DEFAULT_CARD_CIPHER_VIEW
|
val cipherView = DEFAULT_CARD_CIPHER_VIEW
|
||||||
|
|
||||||
val result = cipherView.toViewState()
|
val result = cipherView.toViewState(
|
||||||
|
isClone = false,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultAddEditState.ViewState.Content(
|
VaultAddEditState.ViewState.Content(
|
||||||
|
@ -85,7 +94,10 @@ class CipherViewExtensionsTest {
|
||||||
fun `toViewState should create a Identity ViewState`() {
|
fun `toViewState should create a Identity ViewState`() {
|
||||||
val cipherView = DEFAULT_IDENTITY_CIPHER_VIEW
|
val cipherView = DEFAULT_IDENTITY_CIPHER_VIEW
|
||||||
|
|
||||||
val result = cipherView.toViewState()
|
val result = cipherView.toViewState(
|
||||||
|
isClone = false,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultAddEditState.ViewState.Content(
|
VaultAddEditState.ViewState.Content(
|
||||||
|
@ -131,7 +143,10 @@ class CipherViewExtensionsTest {
|
||||||
fun `toViewState should create a Login ViewState`() {
|
fun `toViewState should create a Login ViewState`() {
|
||||||
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW
|
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW
|
||||||
|
|
||||||
val result = cipherView.toViewState()
|
val result = cipherView.toViewState(
|
||||||
|
isClone = false,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultAddEditState.ViewState.Content(
|
VaultAddEditState.ViewState.Content(
|
||||||
|
@ -172,7 +187,10 @@ class CipherViewExtensionsTest {
|
||||||
fun `toViewState should create a Secure Notes ViewState`() {
|
fun `toViewState should create a Secure Notes ViewState`() {
|
||||||
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
|
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
|
||||||
|
|
||||||
val result = cipherView.toViewState()
|
val result = cipherView.toViewState(
|
||||||
|
isClone = false,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultAddEditState.ViewState.Content(
|
VaultAddEditState.ViewState.Content(
|
||||||
|
@ -197,6 +215,39 @@ class CipherViewExtensionsTest {
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toViewState with isClone true should append clone text to the cipher name`() {
|
||||||
|
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
|
||||||
|
|
||||||
|
val result = cipherView.toViewState(
|
||||||
|
isClone = true,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultAddEditState.ViewState.Content(
|
||||||
|
common = VaultAddEditState.ViewState.Content.Common(
|
||||||
|
originalCipher = cipherView,
|
||||||
|
name = "cipher - Clone",
|
||||||
|
folderName = R.string.folder_none.asText(),
|
||||||
|
favorite = false,
|
||||||
|
masterPasswordReprompt = true,
|
||||||
|
notes = "Lots of notes",
|
||||||
|
ownership = "",
|
||||||
|
customFieldData = listOf(
|
||||||
|
VaultAddEditState.Custom.BooleanField(TEST_ID, "TestBoolean", false),
|
||||||
|
VaultAddEditState.Custom.TextField(TEST_ID, "TestText", "TestText"),
|
||||||
|
VaultAddEditState.Custom.HiddenField(TEST_ID, "TestHidden", "TestHidden"),
|
||||||
|
),
|
||||||
|
availableFolders = emptyList(),
|
||||||
|
availableOwners = emptyList(),
|
||||||
|
),
|
||||||
|
type = VaultAddEditState.ViewState.Content.ItemType.SecureNotes,
|
||||||
|
),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
|
private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
|
||||||
|
|
|
@ -70,7 +70,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
VaultItemScreen(
|
VaultItemScreen(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
onNavigateToVaultAddEditItem = { onNavigateToVaultEditItemId = it },
|
onNavigateToVaultAddEditItem = { id, _ -> onNavigateToVaultEditItemId = id },
|
||||||
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it },
|
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it },
|
||||||
intentManager = intentManager,
|
intentManager = intentManager,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue