From 70a425dfd80049d449295ae758e719b61ed6e315 Mon Sep 17 00:00:00 2001 From: David Perez Date: Mon, 8 Jan 2024 10:10:06 -0600 Subject: [PATCH] BIT-482: Display share sheet after saving a new send (#532) --- .../vault/repository/VaultRepositoryImpl.kt | 16 ++++-- .../repository/model/CreateSendResult.kt | 6 +- .../feature/send/addsend/AddSendScreen.kt | 6 ++ .../feature/send/addsend/AddSendViewModel.kt | 20 ++++++- .../vault/repository/VaultRepositoryTest.kt | 12 ++-- .../feature/send/addsend/AddSendScreenTest.kt | 18 ++++++ .../send/addsend/AddSendViewModelTest.kt | 55 +++++++++++++------ 7 files changed, 104 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index f15c33af2a..cd60633a3d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -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) }, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateSendResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateSendResult.kt index 4ae3f6b217..b7f604827f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateSendResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateSendResult.kt @@ -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. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt index 72387f7381..4bb1f594c3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt @@ -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() } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt index 13dc76be96..dd5eadf372 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt @@ -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( 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. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index a2633f8a14..bcd27d7546 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -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 diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt index 2bdf91f8f1..3f5a10c1fd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt @@ -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() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val viewModel = mockk(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/", ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt index 249047c311..6be1ce4052 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt @@ -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() - 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() + every { viewState.toSendView() } returns mockSendView + val sendUrl = "www.test.com/send/test" + val resultSendView = mockk { + 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(