mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-2101 BIT-2103: Update export flow for JSON (password protected) (#1188)
This commit is contained in:
parent
3565054a4c
commit
90ff2897f5
5 changed files with 170 additions and 23 deletions
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue