mirror of
https://github.com/bitwarden/android.git
synced 2026-06-02 11:12:00 -05:00
BIT-2129 Show descriptive error message when Send creation fails (#1186)
This commit is contained in:
committed by
Álison Fernandes
parent
90ff2897f5
commit
1150f01666
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.api
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import okhttp3.MultipartBody
|
||||
@@ -28,7 +28,7 @@ interface SendsApi {
|
||||
* Create a file send.
|
||||
*/
|
||||
@POST("sends/file/v2")
|
||||
suspend fun createFileSend(@Body body: SendJsonRequest): Result<SendFileResponseJson>
|
||||
suspend fun createFileSend(@Body body: SendJsonRequest): Result<CreateFileSendResponseJson>
|
||||
|
||||
/**
|
||||
* Updates a send.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents a response from create file send.
|
||||
*/
|
||||
sealed class CreateFileSendResponse {
|
||||
|
||||
/**
|
||||
* Represents the response from a successful create file send request.
|
||||
*
|
||||
* @property createFileJsonResponse Response JSON received from create file send request.
|
||||
*/
|
||||
data class Success(
|
||||
val createFileJsonResponse: CreateFileSendResponseJson,
|
||||
) : CreateFileSendResponse()
|
||||
|
||||
/**
|
||||
* Represents the json body of an invalid create request.
|
||||
*
|
||||
* @property message A general, user-displayable error message.
|
||||
* @property validationErrors a map where each value is a list of error messages for each
|
||||
* key. The values in the array should be used for display to the user, since the keys tend
|
||||
* to come back as nonsense. (eg: empty string key)
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
override val message: String,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
override val validationErrors: Map<String, List<String>>?,
|
||||
) : CreateFileSendResponse(), InvalidJsonResponse
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
|
||||
* Represents the JSON response from creating a new file send.
|
||||
*/
|
||||
@Serializable
|
||||
data class SendFileResponseJson(
|
||||
data class CreateFileSendResponseJson(
|
||||
@SerialName("url")
|
||||
val url: String,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models a response from either "create text send" or "create file send" requests.
|
||||
*/
|
||||
sealed class CreateSendJsonResponse {
|
||||
/**
|
||||
* Represents a successful response from either "create text send" or "create file send"
|
||||
* request.
|
||||
*
|
||||
* @property send The created send object.
|
||||
*/
|
||||
data class Success(val send: SyncResponseJson.Send) : CreateSendJsonResponse()
|
||||
|
||||
/**
|
||||
* Represents the json body of an invalid create request.
|
||||
*
|
||||
* @property message A general, user-displayable error message.
|
||||
* @property validationErrors a map where each value is a list of error messages for each
|
||||
* key. The values in the array should be used for display to the user, since the keys tend
|
||||
* to come back as nonsense. (eg: empty string key)
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
override val message: String,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
override val validationErrors: Map<String, List<String>>?,
|
||||
) : CreateSendJsonResponse(), InvalidJsonResponse
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.model
|
||||
|
||||
/**
|
||||
* Represents the json body of an invalid send json request.
|
||||
*/
|
||||
sealed interface InvalidJsonResponse {
|
||||
|
||||
/**
|
||||
* A general, user-displayable error message.
|
||||
*/
|
||||
val message: String
|
||||
|
||||
/**
|
||||
* a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
*/
|
||||
val validationErrors: Map<String, List<String>>?
|
||||
|
||||
/**
|
||||
* Returns the first error message found in [validationErrors], or [message] if there are no
|
||||
* [validationErrors] present.
|
||||
*/
|
||||
val firstValidationErrorMessage: String?
|
||||
get() = validationErrors
|
||||
?.flatMap { it.value }
|
||||
?.first()
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateSendJsonResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson
|
||||
@@ -14,20 +16,20 @@ interface SendsService {
|
||||
*/
|
||||
suspend fun createTextSend(
|
||||
body: SendJsonRequest,
|
||||
): Result<SyncResponseJson.Send>
|
||||
): Result<CreateSendJsonResponse>
|
||||
|
||||
/**
|
||||
* Attempt to create a file send.
|
||||
*/
|
||||
suspend fun createFileSend(
|
||||
body: SendJsonRequest,
|
||||
): Result<SendFileResponseJson>
|
||||
): Result<CreateFileSendResponse>
|
||||
|
||||
/**
|
||||
* Attempt to upload the given [encryptedFile] associated with the [sendFileResponse].
|
||||
*/
|
||||
suspend fun uploadFile(
|
||||
sendFileResponse: SendFileResponseJson,
|
||||
sendFileResponse: CreateFileSendResponseJson,
|
||||
encryptedFile: ByteArray,
|
||||
): Result<SyncResponseJson.Send>
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenErr
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.AzureApi
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.SendsApi
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateSendJsonResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson
|
||||
@@ -28,11 +30,33 @@ class SendsServiceImpl(
|
||||
private val clock: Clock,
|
||||
private val json: Json,
|
||||
) : SendsService {
|
||||
override suspend fun createTextSend(body: SendJsonRequest): Result<SyncResponseJson.Send> =
|
||||
override suspend fun createTextSend(
|
||||
body: SendJsonRequest,
|
||||
): Result<CreateSendJsonResponse> =
|
||||
sendsApi.createTextSend(body = body)
|
||||
.map { CreateSendJsonResponse.Success(send = it) }
|
||||
.recoverCatching { throwable ->
|
||||
throwable.toBitwardenError()
|
||||
.parseErrorBodyOrNull<CreateSendJsonResponse.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override suspend fun createFileSend(body: SendJsonRequest): Result<SendFileResponseJson> =
|
||||
override suspend fun createFileSend(
|
||||
body: SendJsonRequest,
|
||||
): Result<CreateFileSendResponse> =
|
||||
sendsApi.createFileSend(body = body)
|
||||
.map { CreateFileSendResponse.Success(it) }
|
||||
.recoverCatching { throwable ->
|
||||
throwable.toBitwardenError()
|
||||
.parseErrorBodyOrNull<CreateFileSendResponse.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override suspend fun updateSend(
|
||||
sendId: String,
|
||||
@@ -55,7 +79,7 @@ class SendsServiceImpl(
|
||||
}
|
||||
|
||||
override suspend fun uploadFile(
|
||||
sendFileResponse: SendFileResponseJson,
|
||||
sendFileResponse: CreateFileSendResponseJson,
|
||||
encryptedFile: ByteArray,
|
||||
): Result<SyncResponseJson.Send> {
|
||||
val send = sendFileResponse.sendResponse
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.bitwarden.core.ExportFormat
|
||||
import com.bitwarden.core.FolderView
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.Send
|
||||
import com.bitwarden.core.SendType
|
||||
import com.bitwarden.core.SendView
|
||||
import com.bitwarden.crypto.Kdf
|
||||
@@ -35,10 +36,13 @@ import com.x8bit.bitwarden.data.platform.repository.util.map
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.updateToPendingOrLoading
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateSendJsonResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest
|
||||
@@ -980,11 +984,12 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override suspend fun createSend(
|
||||
sendView: SendView,
|
||||
fileUri: Uri?,
|
||||
): CreateSendResult {
|
||||
val userId = activeUserId ?: return CreateSendResult.Error
|
||||
val userId = activeUserId ?: return CreateSendResult.Error(message = null)
|
||||
return vaultSdkSource
|
||||
.encryptSend(
|
||||
userId = userId,
|
||||
@@ -992,54 +997,33 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
.flatMap { send ->
|
||||
when (send.type) {
|
||||
SendType.TEXT -> {
|
||||
sendsService.createTextSend(body = send.toEncryptedNetworkSend())
|
||||
SendType.TEXT -> sendsService.createTextSend(send.toEncryptedNetworkSend())
|
||||
SendType.FILE -> createFileSend(fileUri, userId, send)
|
||||
}
|
||||
}
|
||||
.map { createSendResponse ->
|
||||
when (createSendResponse) {
|
||||
is CreateSendJsonResponse.Invalid -> {
|
||||
return CreateSendResult.Error(
|
||||
message = createSendResponse.firstValidationErrorMessage,
|
||||
)
|
||||
}
|
||||
|
||||
SendType.FILE -> {
|
||||
val uri = fileUri ?: return@flatMap IllegalArgumentException(
|
||||
"File URI must be present to create a File Send.",
|
||||
)
|
||||
.asFailure()
|
||||
|
||||
fileManager
|
||||
.uriToByteArray(fileUri = uri)
|
||||
.flatMap {
|
||||
vaultSdkSource.encryptBuffer(
|
||||
userId = userId,
|
||||
send = send,
|
||||
fileBuffer = it,
|
||||
)
|
||||
}
|
||||
.flatMap { encryptedFile ->
|
||||
sendsService
|
||||
.createFileSend(
|
||||
body = send.toEncryptedNetworkSend(
|
||||
fileLength = encryptedFile.size,
|
||||
),
|
||||
)
|
||||
.flatMap { sendFileResponse ->
|
||||
sendsService.uploadFile(
|
||||
sendFileResponse = sendFileResponse,
|
||||
encryptedFile = encryptedFile,
|
||||
)
|
||||
}
|
||||
}
|
||||
is CreateSendJsonResponse.Success -> {
|
||||
// Save the send immediately, regardless of whether the decrypt succeeds
|
||||
vaultDiskSource.saveSend(userId = userId, send = createSendResponse.send)
|
||||
createSendResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
.onSuccess {
|
||||
// Save the send immediately, regardless of whether the decrypt succeeds
|
||||
vaultDiskSource.saveSend(userId = userId, send = it)
|
||||
}
|
||||
.flatMap {
|
||||
.flatMap { createSendSuccessResponse ->
|
||||
vaultSdkSource.decryptSend(
|
||||
userId = userId,
|
||||
send = it.toEncryptedSdkSend(),
|
||||
send = createSendSuccessResponse.send.toEncryptedSdkSend(),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { CreateSendResult.Error },
|
||||
onFailure = { CreateSendResult.Error(message = null) },
|
||||
onSuccess = { CreateSendResult.Success(it) },
|
||||
)
|
||||
}
|
||||
@@ -1594,6 +1578,56 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createFileSend(
|
||||
uri: Uri?,
|
||||
userId: String,
|
||||
send: Send,
|
||||
): Result<CreateSendJsonResponse> {
|
||||
uri ?: return IllegalArgumentException(
|
||||
"File URI must be present to create a File Send.",
|
||||
)
|
||||
.asFailure()
|
||||
|
||||
return fileManager
|
||||
.uriToByteArray(fileUri = uri)
|
||||
.flatMap {
|
||||
vaultSdkSource.encryptBuffer(
|
||||
userId = userId,
|
||||
send = send,
|
||||
fileBuffer = it,
|
||||
)
|
||||
}
|
||||
.flatMap { encryptedFile ->
|
||||
sendsService
|
||||
.createFileSend(
|
||||
body = send.toEncryptedNetworkSend(
|
||||
fileLength = encryptedFile.size,
|
||||
),
|
||||
)
|
||||
.flatMap { sendFileResponse ->
|
||||
when (sendFileResponse) {
|
||||
is CreateFileSendResponse.Invalid -> {
|
||||
CreateSendJsonResponse
|
||||
.Invalid(
|
||||
message = sendFileResponse.message,
|
||||
validationErrors = sendFileResponse.validationErrors,
|
||||
)
|
||||
.asSuccess()
|
||||
}
|
||||
|
||||
is CreateFileSendResponse.Success -> {
|
||||
sendsService
|
||||
.uploadFile(
|
||||
sendFileResponse = sendFileResponse.createFileJsonResponse,
|
||||
encryptedFile = encryptedFile,
|
||||
)
|
||||
.map { CreateSendJsonResponse.Success(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the send specified by [syncSendDeleteData] from disk.
|
||||
*/
|
||||
|
||||
@@ -15,5 +15,5 @@ sealed class CreateSendResult {
|
||||
/**
|
||||
* Generic error while creating a send.
|
||||
*/
|
||||
data object Error : CreateSendResult()
|
||||
data class Error(val message: String?) : CreateSendResult()
|
||||
}
|
||||
|
||||
@@ -184,12 +184,13 @@ class AddSendViewModel @Inject constructor(
|
||||
action: AddSendAction.Internal.CreateSendResultReceive,
|
||||
) {
|
||||
when (val result = action.result) {
|
||||
CreateSendResult.Error -> {
|
||||
is CreateSendResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = AddSendState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
message = result.message?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user