mirror of
https://github.com/bitwarden/android.git
synced 2026-06-01 02:06:52 -05:00
BIT-2235: Add support for exporting vault data w/o passcode (#1281)
This commit is contained in:
committed by
Álison Fernandes
parent
9648f720be
commit
dae98111e6
@@ -26,10 +26,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
@@ -177,6 +178,9 @@ fun ExportVaultScreen(
|
||||
onPasswordInputChanged = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(it)) }
|
||||
},
|
||||
onSendCodeClicked = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ExportVaultAction.SendCodeClick) }
|
||||
},
|
||||
onExportVaultClick = { shouldShowConfirmationDialog = true },
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
@@ -193,6 +197,7 @@ private fun ExportVaultScreenContent(
|
||||
onExportFormatOptionSelected: (ExportVaultFormat) -> Unit,
|
||||
onFilePasswordInputChanged: (String) -> Unit,
|
||||
onPasswordInputChanged: (String) -> Unit,
|
||||
onSendCodeClicked: () -> Unit,
|
||||
onExportVaultClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -207,7 +212,7 @@ private fun ExportVaultScreenContent(
|
||||
BitwardenPolicyWarningText(
|
||||
text = stringResource(id = R.string.disable_personal_vault_export_policy_in_effect),
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "DisablePrivateVaultPolicyLabel" }
|
||||
.testTag("DisablePrivateVaultPolicyLabel")
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
@@ -228,7 +233,7 @@ private fun ExportVaultScreenContent(
|
||||
},
|
||||
isEnabled = !state.policyPreventsExport,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "FileFormatPicker" }
|
||||
.testTag("FileFormatPicker")
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
@@ -245,7 +250,7 @@ private fun ExportVaultScreenContent(
|
||||
showPasswordChange = { showPassword = it },
|
||||
hint = stringResource(id = R.string.password_used_to_export),
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "FilePasswordEntry" }
|
||||
.testTag("FilePasswordEntry")
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
@@ -264,44 +269,87 @@ private fun ExportVaultScreenContent(
|
||||
showPassword = showPassword,
|
||||
showPasswordChange = { showPassword = it },
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "ConfirmFilePasswordEntry" }
|
||||
.testTag("ConfirmFilePasswordEntry")
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password),
|
||||
value = state.passwordInput,
|
||||
readOnly = state.policyPreventsExport,
|
||||
onValueChange = onPasswordInputChanged,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "MasterPasswordEntry" }
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
if (state.showSendCodeButton) {
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.export_vault_master_password_description),
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_verification_code_to_email),
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
BitwardenFilledTonalButton(
|
||||
label = stringResource(R.string.send_code),
|
||||
onClick = onSendCodeClicked,
|
||||
isEnabled = !state.policyPreventsExport,
|
||||
modifier = Modifier
|
||||
.testTag("SendTOTPCodeButton")
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.verification_code),
|
||||
value = state.passwordInput,
|
||||
readOnly = state.policyPreventsExport,
|
||||
hint = stringResource(id = R.string.confirm_your_identity),
|
||||
onValueChange = onPasswordInputChanged,
|
||||
keyboardType = KeyboardType.Number,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
} else {
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password),
|
||||
value = state.passwordInput,
|
||||
readOnly = state.policyPreventsExport,
|
||||
onValueChange = onPasswordInputChanged,
|
||||
modifier = Modifier
|
||||
.testTag("MasterPasswordEntry")
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.export_vault_master_password_description),
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
BitwardenFilledTonalButton(
|
||||
label = stringResource(id = R.string.export_vault),
|
||||
onClick = onExportVaultClick,
|
||||
isEnabled = !state.policyPreventsExport,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "ExportVaultButton" }
|
||||
.testTag("ExportVaultButton")
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
@@ -42,7 +43,7 @@ private const val KEY_STATE = "state"
|
||||
@HiltViewModel
|
||||
class ExportVaultViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val policyManager: PolicyManager,
|
||||
policyManager: PolicyManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val fileManager: FileManager,
|
||||
@@ -61,6 +62,12 @@ class ExportVaultViewModel @Inject constructor(
|
||||
policyPreventsExport = policyManager
|
||||
.getActivePolicies(type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT)
|
||||
.any(),
|
||||
showSendCodeButton = authRepository
|
||||
.userStateFlow
|
||||
.value
|
||||
?.activeAccount
|
||||
?.trustedDevice
|
||||
?.hasMasterPassword == false,
|
||||
),
|
||||
) {
|
||||
/**
|
||||
@@ -88,6 +95,7 @@ class ExportVaultViewModel @Inject constructor(
|
||||
is ExportVaultAction.FilePasswordInputChange -> handleFilePasswordInputChanged(action)
|
||||
is ExportVaultAction.ExportFormatOptionSelect -> handleExportFormatOptionSelect(action)
|
||||
is ExportVaultAction.PasswordInputChanged -> handlePasswordInputChanged(action)
|
||||
ExportVaultAction.SendCodeClick -> handleSendCodeClick()
|
||||
is ExportVaultAction.ExportLocationReceive -> handleExportLocationReceive(action)
|
||||
|
||||
is ExportVaultAction.Internal.ReceiveValidatePasswordResult -> {
|
||||
@@ -105,6 +113,10 @@ class ExportVaultViewModel @Inject constructor(
|
||||
is ExportVaultAction.Internal.SaveExportDataToUriResultReceive -> {
|
||||
handleExportDataFinishedSavingToDisk(action)
|
||||
}
|
||||
|
||||
is ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult -> {
|
||||
handleReceiveVerifyOneTimePasscodeResult(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,11 +170,19 @@ class ExportVaultViewModel @Inject constructor(
|
||||
// Otherwise, validate the password.
|
||||
viewModelScope.launch {
|
||||
sendAction(
|
||||
ExportVaultAction.Internal.ReceiveValidatePasswordResult(
|
||||
result = authRepository.validatePassword(
|
||||
password = state.passwordInput,
|
||||
),
|
||||
),
|
||||
if (state.showSendCodeButton) {
|
||||
ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult(
|
||||
result = authRepository.verifyOneTimePasscode(
|
||||
oneTimePasscode = state.passwordInput,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ExportVaultAction.Internal.ReceiveValidatePasswordResult(
|
||||
result = authRepository.validatePassword(
|
||||
password = state.passwordInput,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -248,6 +268,12 @@ class ExportVaultViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendCodeClick() {
|
||||
viewModelScope.launch {
|
||||
authRepository.requestOneTimePasscode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an alert or proceed to export the vault after validating the password.
|
||||
*/
|
||||
@@ -266,27 +292,7 @@ class ExportVaultViewModel @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = ExportVaultState.DialogState.Loading())
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = vaultRepository.exportVaultDataToString(
|
||||
format = state.exportFormat.toExportFormat(
|
||||
password = if (state.exportFormat == ExportVaultFormat.JSON_ENCRYPTED) {
|
||||
state.filePasswordInput
|
||||
} else {
|
||||
state.passwordInput
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
sendAction(
|
||||
ExportVaultAction.Internal.ReceiveExportVaultDataToStringResult(
|
||||
result = result,
|
||||
),
|
||||
)
|
||||
}
|
||||
exportVaultData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,6 +372,45 @@ class ExportVaultViewModel @Inject constructor(
|
||||
sendEvent(ExportVaultEvent.ShowToast(R.string.export_vault_success.asText()))
|
||||
}
|
||||
|
||||
private fun handleReceiveVerifyOneTimePasscodeResult(
|
||||
action: ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult,
|
||||
) {
|
||||
when (action.result) {
|
||||
VerifyOtpResult.Verified -> exportVaultData()
|
||||
|
||||
is VerifyOtpResult.NotVerified -> {
|
||||
updateStateWithError(R.string.generic_error_message.asText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles exporting the vault data after all validation has finished.
|
||||
*/
|
||||
private fun exportVaultData() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = ExportVaultState.DialogState.Loading())
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = vaultRepository.exportVaultDataToString(
|
||||
format = state.exportFormat.toExportFormat(
|
||||
password = if (state.exportFormat == ExportVaultFormat.JSON_ENCRYPTED) {
|
||||
state.filePasswordInput
|
||||
} else {
|
||||
state.passwordInput
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
sendAction(
|
||||
ExportVaultAction.Internal.ReceiveExportVaultDataToStringResult(
|
||||
result = result,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateWithError(message: Text) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
@@ -393,6 +438,7 @@ data class ExportVaultState(
|
||||
val passwordInput: String,
|
||||
val passwordStrengthState: PasswordStrengthState,
|
||||
val policyPreventsExport: Boolean,
|
||||
val showSendCodeButton: Boolean,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
@@ -484,6 +530,11 @@ sealed class ExportVaultAction {
|
||||
*/
|
||||
data class PasswordInputChanged(val input: String) : ExportVaultAction()
|
||||
|
||||
/**
|
||||
* Indicates that the user pressed the button to send a code in place of entering a password.
|
||||
*/
|
||||
data object SendCodeClick : ExportVaultAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [ExportVaultViewModel] might send itself.
|
||||
*/
|
||||
@@ -516,5 +567,12 @@ sealed class ExportVaultAction {
|
||||
data class ReceiveValidatePasswordResult(
|
||||
val result: ValidatePasswordResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a result for verifying the one-time passcode has been received.
|
||||
*/
|
||||
data class ReceiveVerifyOneTimePasscodeResult(
|
||||
val result: VerifyOtpResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user