BIT-482: Display share sheet after saving a new send (#532)

This commit is contained in:
David Perez 2024-01-08 10:10:06 -06:00 committed by Álison Fernandes
parent 185849951f
commit 70a425dfd8
7 changed files with 104 additions and 29 deletions

View file

@ -43,6 +43,7 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
import kotlinx.coroutines.CoroutineScope
@ -435,12 +436,19 @@ class VaultRepositoryImpl(
sendView = sendView,
)
.flatMap { send -> sendsService.createSend(body = send.toEncryptedNetworkSend()) }
.onSuccess {
// Save the send immediately, regardless of whether the decrypt succeeds
vaultDiskSource.saveSend(userId = userId, send = it)
}
.flatMap {
vaultSdkSource.decryptSend(
userId = userId,
send = it.toEncryptedSdkSend(),
)
}
.fold(
onFailure = { CreateSendResult.Error },
onSuccess = {
vaultDiskSource.saveSend(userId = userId, send = it)
CreateSendResult.Success
},
onSuccess = { CreateSendResult.Success(it) },
)
}

View file

@ -1,14 +1,16 @@
package com.x8bit.bitwarden.data.vault.repository.model
import com.bitwarden.core.SendView
/**
* Models result of creating a send.
*/
sealed class CreateSendResult {
/**
* send created successfully.
* Send created successfully and contains the decrypted [SendView].
*/
data object Success : CreateSendResult()
data class Success(val sendView: SendView) : CreateSendResult()
/**
* Generic error while creating a send.

View file

@ -19,6 +19,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
@ -38,6 +39,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandler
@Composable
fun AddSendScreen(
viewModel: AddSendViewModel = hiltViewModel(),
intentHandler: IntentHandler = IntentHandler(LocalContext.current),
onNavigateBack: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -47,6 +49,10 @@ fun AddSendScreen(
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is AddSendEvent.NavigateBack -> onNavigateBack()
is AddSendEvent.ShowShareSheet -> {
intentHandler.shareText(event.message)
}
is AddSendEvent.ShowToast -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}

View file

@ -6,12 +6,15 @@ import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
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
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendView
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@ -31,6 +34,7 @@ private const val KEY_STATE = "state"
class AddSendViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
authRepo: AuthRepository,
private val environmentRepo: EnvironmentRepository,
private val vaultRepo: VaultRepository,
) : BaseViewModel<AddSendState, AddSendEvent, AddSendAction>(
initialState = savedStateHandle[KEY_STATE] ?: AddSendState(
@ -50,6 +54,7 @@ class AddSendViewModel @Inject constructor(
),
dialogState = null,
isPremiumUser = authRepo.userStateFlow.value?.activeAccount?.isPremium == true,
baseWebSendUrl = environmentRepo.environment.environmentUrlData.baseWebSendUrl,
),
) {
@ -91,7 +96,7 @@ class AddSendViewModel @Inject constructor(
private fun handleCreateSendResultReceive(
action: AddSendAction.Internal.CreateSendResultReceive,
) {
when (action.result) {
when (val result = action.result) {
CreateSendResult.Error -> {
mutableStateFlow.update {
it.copy(
@ -103,9 +108,14 @@ class AddSendViewModel @Inject constructor(
}
}
CreateSendResult.Success -> {
is CreateSendResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(AddSendEvent.NavigateBack)
sendEvent(
AddSendEvent.ShowShareSheet(
message = result.sendView.toSendUrl(state.baseWebSendUrl),
),
)
}
}
}
@ -287,6 +297,7 @@ data class AddSendState(
val dialogState: DialogState?,
val viewState: ViewState,
val isPremiumUser: Boolean,
val baseWebSendUrl: String,
) : Parcelable {
/**
@ -383,6 +394,11 @@ sealed class AddSendEvent {
*/
data object NavigateBack : AddSendEvent()
/**
* Show share sheet.
*/
data class ShowShareSheet(val message: String) : AddSendEvent()
/**
* Show Toast.
*/

View file

@ -1904,24 +1904,28 @@ class VaultRepositoryTest {
}
@Test
@Suppress("MaxLineLength")
fun `createSend with sendsService createSend success should return CreateSendResult success`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val mockSendView = createMockSendView(number = 1)
val mockSdkSend = createMockSdkSend(number = 1)
val mockSend = createMockSend(number = 1)
val mockSendViewResult = createMockSendView(number = 2)
coEvery {
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
} returns createMockSdkSend(number = 1).asSuccess()
val mockSend = createMockSend(number = 1)
} returns mockSdkSend.asSuccess()
coEvery {
sendsService.createSend(body = createMockSendJsonRequest(number = 1))
} returns mockSend.asSuccess()
coEvery { vaultDiskSource.saveSend(userId, mockSend) } just runs
coEvery {
vaultSdkSource.decryptSend(userId, mockSdkSend)
} returns mockSendViewResult.asSuccess()
val result = vaultRepository.createSend(sendView = mockSendView)
assertEquals(CreateSendResult.Success, result)
assertEquals(CreateSendResult.Success(mockSendViewResult), result)
}
@Test

View file

@ -17,10 +17,13 @@ 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.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.util.isProgressBar
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@ -31,6 +34,10 @@ import org.junit.Test
class AddSendScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private val intentHandler: IntentHandler = mockk {
every { shareText(any()) } just runs
}
private val mutableEventFlow = bufferedMutableSharedFlow<AddSendEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<AddSendViewModel>(relaxed = true) {
@ -43,6 +50,7 @@ class AddSendScreenTest : BaseComposeTest() {
composeTestRule.setContent {
AddSendScreen(
viewModel = viewModel,
intentHandler = intentHandler,
onNavigateBack = { onNavigateBackCalled = true },
)
}
@ -54,6 +62,15 @@ class AddSendScreenTest : BaseComposeTest() {
assert(onNavigateBackCalled)
}
@Test
fun `on ShowShareSheet should call shareText on IntentHandler`() {
val text = "sharable stuff"
mutableEventFlow.tryEmit(AddSendEvent.ShowShareSheet(text))
verify {
intentHandler.shareText(text)
}
}
@Test
fun `on close icon click should send CloseClick`() {
composeTestRule
@ -602,6 +619,7 @@ class AddSendScreenTest : BaseComposeTest() {
viewState = DEFAULT_VIEW_STATE,
dialogState = null,
isPremiumUser = false,
baseWebSendUrl = "https://vault.bitwarden.com/#/send/",
)
}
}

View file

@ -6,12 +6,14 @@ import com.bitwarden.core.SendView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendView
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@ -31,16 +33,21 @@ class AddSendViewModelTest : BaseViewModelTest() {
private val authRepository: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
}
private val environmentRepository: EnvironmentRepository = mockk {
every { environment } returns Environment.Us
}
private val vaultRepository: VaultRepository = mockk()
@BeforeEach
fun setup() {
mockkStatic(ADD_SEND_STATE_EXTENSIONS_PATH)
mockkStatic(SEND_VIEW_EXTENSIONS_PATH)
}
@AfterEach
fun tearDown() {
unmockkStatic(ADD_SEND_STATE_EXTENSIONS_PATH)
unmockkStatic(SEND_VIEW_EXTENSIONS_PATH)
}
@Test
@ -68,25 +75,33 @@ class AddSendViewModelTest : BaseViewModelTest() {
}
@Test
fun `SaveClick with createSend success should emit NavigateBack`() = runTest {
val viewState = DEFAULT_VIEW_STATE.copy(
common = DEFAULT_COMMON_STATE.copy(name = "input"),
)
val initialState = DEFAULT_STATE.copy(viewState = viewState)
val mockSendView = mockk<SendView>()
every { viewState.toSendView() } returns mockSendView
coEvery { vaultRepository.createSend(mockSendView) } returns CreateSendResult.Success
val viewModel = createViewModel(initialState)
fun `SaveClick with createSend success should emit NavigateBack and ShowShareSheet`() =
runTest {
val viewState = DEFAULT_VIEW_STATE.copy(
common = DEFAULT_COMMON_STATE.copy(name = "input"),
)
val initialState = DEFAULT_STATE.copy(viewState = viewState)
val mockSendView = mockk<SendView>()
every { viewState.toSendView() } returns mockSendView
val sendUrl = "www.test.com/send/test"
val resultSendView = mockk<SendView> {
every { toSendUrl(DEFAULT_ENVIRONMENT_URL) } returns sendUrl
}
coEvery {
vaultRepository.createSend(mockSendView)
} returns CreateSendResult.Success(sendView = resultSendView)
val viewModel = createViewModel(initialState)
viewModel.eventFlow.test {
viewModel.trySendAction(AddSendAction.SaveClick)
assertEquals(AddSendEvent.NavigateBack, awaitItem())
viewModel.eventFlow.test {
viewModel.trySendAction(AddSendAction.SaveClick)
assertEquals(AddSendEvent.NavigateBack, awaitItem())
assertEquals(AddSendEvent.ShowShareSheet(sendUrl), awaitItem())
}
assertEquals(initialState, viewModel.stateFlow.value)
coVerify(exactly = 1) {
vaultRepository.createSend(mockSendView)
}
}
assertEquals(initialState, viewModel.stateFlow.value)
coVerify(exactly = 1) {
vaultRepository.createSend(mockSendView)
}
}
@Test
fun `SaveClick with createSend failure should show error dialog`() = runTest {
@ -313,12 +328,15 @@ class AddSendViewModelTest : BaseViewModelTest() {
): AddSendViewModel = AddSendViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
authRepo = authRepository,
environmentRepo = environmentRepository,
vaultRepo = vaultRepository,
)
companion object {
private const val ADD_SEND_STATE_EXTENSIONS_PATH: String =
"com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.AddSendStateExtensionsKt"
private const val SEND_VIEW_EXTENSIONS_PATH: String =
"com.x8bit.bitwarden.ui.tools.feature.send.util.SendViewExtensionsKt"
private val DEFAULT_COMMON_STATE = AddSendState.ViewState.Content.Common(
name = "",
@ -339,10 +357,13 @@ class AddSendViewModelTest : BaseViewModelTest() {
selectedType = DEFAULT_SELECTED_TYPE_STATE,
)
private const val DEFAULT_ENVIRONMENT_URL = "https://vault.bitwarden.com/#/send/"
private val DEFAULT_STATE = AddSendState(
viewState = DEFAULT_VIEW_STATE,
dialogState = null,
isPremiumUser = false,
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
)
private val DEFAULT_USER_ACCOUNT_STATE = UserState.Account(