BIT-2101 BIT-2103: Update export flow for JSON (password protected) (#1188)

This commit is contained in:
Caleb Derosier 2024-03-29 13:51:17 -06:00 committed by Álison Fernandes
parent 3565054a4c
commit 90ff2897f5
5 changed files with 170 additions and 23 deletions

View file

@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.createaccount.PasswordStrengthIndicator
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
@ -164,9 +165,15 @@ fun ExportVaultScreen(
) { innerPadding ->
ExportVaultScreenContent(
state = state,
onConfirmFilePasswordInputChanged = remember(viewModel) {
{ viewModel.trySendAction(ExportVaultAction.ConfirmFilePasswordInputChange(it)) }
},
onExportFormatOptionSelected = remember(viewModel) {
{ viewModel.trySendAction(ExportVaultAction.ExportFormatOptionSelect(it)) }
},
onFilePasswordInputChanged = remember(viewModel) {
{ viewModel.trySendAction(ExportVaultAction.FilePasswordInputChange(it)) }
},
onPasswordInputChanged = remember(viewModel) {
{ viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(it)) }
},
@ -182,7 +189,9 @@ fun ExportVaultScreen(
@Suppress("LongMethod")
private fun ExportVaultScreenContent(
state: ExportVaultState,
onConfirmFilePasswordInputChanged: (String) -> Unit,
onExportFormatOptionSelected: (ExportVaultFormat) -> Unit,
onFilePasswordInputChanged: (String) -> Unit,
onPasswordInputChanged: (String) -> Unit,
onExportVaultClick: () -> Unit,
modifier: Modifier = Modifier,
@ -226,6 +235,37 @@ private fun ExportVaultScreenContent(
Spacer(modifier = Modifier.height(8.dp))
if (state.exportFormat == ExportVaultFormat.JSON_ENCRYPTED) {
BitwardenPasswordField(
label = stringResource(id = R.string.file_password),
value = state.filePasswordInput,
onValueChange = onFilePasswordInputChanged,
hint = stringResource(id = R.string.password_used_to_export),
modifier = Modifier
.semantics { testTag = "FilePasswordEntry" }
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp),
state = state.passwordStrengthState,
)
Spacer(modifier = Modifier.height(4.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.confirm_file_password),
value = state.confirmFilePasswordInput,
onValueChange = onConfirmFilePasswordInputChanged,
modifier = Modifier
.semantics { testTag = "ConfirmFilePasswordEntry" }
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
}
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
value = state.passwordInput,

View file

@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
import com.x8bit.bitwarden.ui.auth.feature.createaccount.PasswordStrengthState
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.asText
@ -46,10 +47,13 @@ class ExportVaultViewModel @Inject constructor(
) : BaseViewModel<ExportVaultState, ExportVaultEvent, ExportVaultAction>(
initialState = savedStateHandle[KEY_STATE]
?: ExportVaultState(
confirmFilePasswordInput = "",
dialogState = null,
exportData = null,
exportFormat = ExportVaultFormat.JSON,
filePasswordInput = "",
passwordInput = "",
passwordStrengthState = PasswordStrengthState.NONE,
policyPreventsExport = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT)
.any(),
@ -66,7 +70,12 @@ class ExportVaultViewModel @Inject constructor(
when (action) {
ExportVaultAction.CloseButtonClick -> handleCloseButtonClicked()
ExportVaultAction.ConfirmExportVaultClicked -> handleConfirmExportVaultClicked()
is ExportVaultAction.ConfirmFilePasswordInputChange -> {
handleConfirmFilePasswordInputChanged(action)
}
ExportVaultAction.DialogDismiss -> handleDialogDismiss()
is ExportVaultAction.FilePasswordInputChange -> handleFilePasswordInputChanged(action)
is ExportVaultAction.ExportFormatOptionSelect -> handleExportFormatOptionSelect(action)
is ExportVaultAction.PasswordInputChanged -> handlePasswordInputChanged(action)
is ExportVaultAction.ExportLocationReceive -> handleExportLocationReceive(action)
@ -118,6 +127,17 @@ class ExportVaultViewModel @Inject constructor(
}
}
/**
* Update the state with the new confirm file password input.
*/
private fun handleConfirmFilePasswordInputChanged(
action: ExportVaultAction.ConfirmFilePasswordInputChange,
) {
mutableStateFlow.update {
it.copy(confirmFilePasswordInput = action.input)
}
}
/**
* Dismiss the dialog.
*/
@ -155,6 +175,15 @@ class ExportVaultViewModel @Inject constructor(
}
}
/**
* Update the state with the new file password input.
*/
private fun handleFilePasswordInputChanged(action: ExportVaultAction.FilePasswordInputChange) {
mutableStateFlow.update {
it.copy(filePasswordInput = action.input)
}
}
/**
* Update the state with the new password input.
*/
@ -267,9 +296,12 @@ class ExportVaultViewModel @Inject constructor(
data class ExportVaultState(
@IgnoredOnParcel
val exportData: String? = null,
val confirmFilePasswordInput: String,
val dialogState: DialogState?,
val exportFormat: ExportVaultFormat,
val filePasswordInput: String,
val passwordInput: String,
val passwordStrengthState: PasswordStrengthState,
val policyPreventsExport: Boolean,
) : Parcelable {
/**
@ -330,6 +362,11 @@ sealed class ExportVaultAction {
*/
data object ConfirmExportVaultClicked : ExportVaultAction()
/**
* Indicates that the confirm file password input has changed.
*/
data class ConfirmFilePasswordInputChange(val input: String) : ExportVaultAction()
/**
* Indicates that the dialog has been dismissed.
*/
@ -347,6 +384,11 @@ sealed class ExportVaultAction {
val fileUri: Uri,
) : ExportVaultAction()
/**
* Indicates that the file password input has changed.
*/
data class FilePasswordInputChange(val input: String) : ExportVaultAction()
/**
* Indicates that the password input has changed.
*/

View file

@ -6,9 +6,12 @@
<string name="duo_org_title" translatable="false">Duo (%1$s)</string>
<string name="json_extension" translatable="false">.json</string>
<string name="json_extension_formatted" translatable="false">.json (%1$s)</string>
<!-- TODO BIT-2140: Update typeform strings -->
<string name="give_feedback" translatable="false">Give Feedback</string>
<!-- TODO BIT-2140: Update typeform and vault export strings -->
<string name="confirm_file_password" translatable="false">Confirm file password</string>
<string name="continue_to_give_feedback" translatable="false">Continue to Give Feedback?</string>
<string name="continue_to_provide_feedback" translatable="false">Select continue to provide feedback on your experience in a web form.</string>
<string name="file_password" translatable="false">File password</string>
<string name="give_feedback" translatable="false">Give Feedback</string>
<string name="password_protected" translatable="false">Password Protected</string>
<string name="password_used_to_export" translatable="false">This password will be used to export and import this file</string>
</resources>

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.exportvault
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
@ -14,6 +15,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.auth.feature.createaccount.PasswordStrengthState
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.feature.settings.exportvault.model.ExportVaultFormat
@ -215,6 +217,36 @@ class ExportVaultScreenTest : BaseComposeTest() {
assertTrue(onNavigateBackCalled)
}
@Test
fun `confirm file password input change should send ConfirmFilePasswordInputChange action`() {
composeTestRule.onNodeWithText("Confirm file password").assertIsNotDisplayed()
mutableStateFlow.update {
it.copy(
exportFormat = ExportVaultFormat.JSON_ENCRYPTED,
)
}
val input = "Test123"
composeTestRule.onNodeWithText("Confirm file password").performTextInput(input)
verify {
viewModel.trySendAction(ExportVaultAction.ConfirmFilePasswordInputChange("Test123"))
}
}
@Test
fun `file password input change should send FilePasswordInputChange action`() {
composeTestRule.onNodeWithText("File password").assertIsNotDisplayed()
mutableStateFlow.update {
it.copy(
exportFormat = ExportVaultFormat.JSON_ENCRYPTED,
)
}
val input = "Test123"
composeTestRule.onNodeWithText("File password").performTextInput(input)
verify {
viewModel.trySendAction(ExportVaultAction.FilePasswordInputChange("Test123"))
}
}
@Test
fun `password input change should send PasswordInputChange action`() {
val input = "Test123"
@ -226,9 +258,12 @@ class ExportVaultScreenTest : BaseComposeTest() {
}
private val DEFAULT_STATE = ExportVaultState(
confirmFilePasswordInput = "",
dialogState = null,
exportFormat = ExportVaultFormat.JSON,
filePasswordInput = "",
passwordInput = "",
exportData = "",
passwordStrengthState = PasswordStrengthState.NONE,
policyPreventsExport = false,
)

View file

@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
import com.x8bit.bitwarden.ui.auth.feature.createaccount.PasswordStrengthState
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.feature.settings.exportvault.model.ExportVaultFormat
@ -194,18 +195,42 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
}
@Test
fun `PasswordInputChanged should update the password input in the state`() = runTest {
fun `ConfirmFilePasswordInputChanged should update the confirm password input in the state`() {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged("Test123"))
viewModel.trySendAction(ExportVaultAction.ConfirmFilePasswordInputChange("Test123"))
assertEquals(
DEFAULT_STATE.copy(
passwordInput = "Test123",
),
viewModel.stateFlow.value,
)
}
assertEquals(
DEFAULT_STATE.copy(
confirmFilePasswordInput = "Test123",
),
viewModel.stateFlow.value,
)
}
@Test
fun `FilePasswordInputChanged should update the file password input in the state`() {
val viewModel = createViewModel()
viewModel.trySendAction(ExportVaultAction.FilePasswordInputChange("Test123"))
assertEquals(
DEFAULT_STATE.copy(
filePasswordInput = "Test123",
),
viewModel.stateFlow.value,
)
}
@Test
fun `PasswordInputChanged should update the password input in the state`() {
val viewModel = createViewModel()
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged("Test123"))
assertEquals(
DEFAULT_STATE.copy(
passwordInput = "Test123",
),
viewModel.stateFlow.value,
)
}
@Test
@ -327,23 +352,25 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
private fun createViewModel(
initialState: ExportVaultState? = null,
): ExportVaultViewModel =
ExportVaultViewModel(
authRepository = authRepository,
policyManager = policyManager,
savedStateHandle = SavedStateHandle(
initialState = mapOf("state" to initialState),
),
fileManager = fileManager,
vaultRepository = vaultRepository,
clock = clock,
)
): ExportVaultViewModel = ExportVaultViewModel(
authRepository = authRepository,
policyManager = policyManager,
savedStateHandle = SavedStateHandle(
initialState = mapOf("state" to initialState),
),
fileManager = fileManager,
vaultRepository = vaultRepository,
clock = clock,
)
}
private val DEFAULT_STATE = ExportVaultState(
confirmFilePasswordInput = "",
dialogState = null,
exportFormat = ExportVaultFormat.JSON,
filePasswordInput = "",
passwordInput = "",
exportData = null,
passwordStrengthState = PasswordStrengthState.NONE,
policyPreventsExport = false,
)