diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/IntentHandler.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/IntentHandler.kt index 169dd512cd..14319123cc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/IntentHandler.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/IntentHandler.kt @@ -52,4 +52,15 @@ class IntentHandler(private val context: Context) { } startActivity(Intent(Intent.ACTION_VIEW, newUri)) } + + /** + * Launches the share sheet with the given [text]. + */ + fun shareText(text: String) { + val sendIntent: Intent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_TEXT, text) + type = "text/plain" + } + startActivity(Intent.createChooser(sendIntent, null)) + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt index 71e78bd48f..3d6b2e3975 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt @@ -29,10 +29,12 @@ 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.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent +import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem +import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers import kotlinx.collections.immutable.persistentListOf @@ -58,6 +60,10 @@ fun SendScreen( intentHandler.launchUri("https://bitwarden.com/products/send".toUri()) } + is SendEvent.ShowShareSheet -> { + intentHandler.shareText(event.url) + } + is SendEvent.ShowToast -> { Toast .makeText(context, event.message(context.resources), Toast.LENGTH_SHORT) @@ -66,6 +72,10 @@ fun SendScreen( } } + SendDialogs( + dialogState = state.dialogState, + ) + val sendHandlers = remember(viewModel) { SendHandlers.create(viewModel) } val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( state = rememberTopAppBarState(), @@ -159,3 +169,16 @@ fun SendScreen( } } } + +@Composable +private fun SendDialogs( + dialogState: SendState.DialogState?, +) { + when (dialogState) { + is SendState.DialogState.Loading -> BitwardenLoadingDialog( + visibilityState = LoadingDialogState.Shown(dialogState.message), + ) + + null -> Unit + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt index 2f057d80db..861ff5835e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt @@ -43,6 +43,7 @@ class SendViewModel @Inject constructor( initialState = savedStateHandle[KEY_STATE] ?: SendState( viewState = SendState.ViewState.Loading, + dialogState = null, ), ) { @@ -81,6 +82,7 @@ class SendViewModel @Inject constructor( viewState = SendState.ViewState.Error( message = R.string.generic_error_message.asText(), ), + dialogState = null, ) } } @@ -94,6 +96,7 @@ class SendViewModel @Inject constructor( .environmentUrlData .baseWebSendUrl, ), + dialogState = null, ) } } @@ -112,6 +115,7 @@ class SendViewModel @Inject constructor( .asText() .concat(R.string.internet_connection_required_message.asText()), ), + dialogState = null, ) } } @@ -154,7 +158,9 @@ class SendViewModel @Inject constructor( } private fun handleSyncClick() { - // TODO: Add loading dialog state BIT-481 + mutableStateFlow.update { + it.copy(dialogState = SendState.DialogState.Loading(R.string.syncing.asText())) + } vaultRepo.sync() } @@ -163,22 +169,21 @@ class SendViewModel @Inject constructor( } private fun handleSendClick(action: SendAction.SendClick) { - // TODO: Navigate to the edit send screen BIT-?? + // TODO: Navigate to the edit send screen (BIT-1387) sendEvent(SendEvent.ShowToast("Not yet implemented".asText())) } private fun handleShareClick(action: SendAction.ShareClick) { - // TODO: Create a link and use the share sheet BIT-?? - sendEvent(SendEvent.ShowToast("Not yet implemented".asText())) + sendEvent(SendEvent.ShowShareSheet(action.sendItem.shareUrl)) } private fun handleFileTypeClick() { - // TODO: Navigate to the file type send list screen BIT-?? + // TODO: Navigate to the file type send list screen (BIT-1388) sendEvent(SendEvent.ShowToast("Not yet implemented".asText())) } private fun handleTextTypeClick() { - // TODO: Navigate to the text type send list screen BIT-?? + // TODO: Navigate to the text type send list screen (BIT-1388) sendEvent(SendEvent.ShowToast("Not yet implemented".asText())) } } @@ -189,6 +194,7 @@ class SendViewModel @Inject constructor( @Parcelize data class SendState( val viewState: ViewState, + val dialogState: DialogState?, ) : Parcelable { /** @@ -259,6 +265,20 @@ data class SendState( override val shouldDisplayFab: Boolean get() = false } } + + /** + * Represents the current state of any dialogs on the screen. + */ + sealed class DialogState : Parcelable { + + /** + * Represents a loading dialog with the given [message]. + */ + @Parcelize + data class Loading( + val message: Text, + ) : DialogState() + } } /** @@ -353,6 +373,11 @@ sealed class SendEvent { */ data object NavigateToAboutSend : SendEvent() + /** + * Show a share sheet with the given content. + */ + data class ShowShareSheet(val url: String) : SendEvent() + /** * Show a toast to the user. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt index 8e4ac84852..d003c7a2b2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt @@ -41,6 +41,7 @@ class SendScreenTest : BaseComposeTest() { private val intentHandler = mockk { every { launchUri(any()) } just runs + every { shareText(any()) } just runs } private val mutableEventFlow = bufferedMutableSharedFlow() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) @@ -74,6 +75,15 @@ class SendScreenTest : BaseComposeTest() { } } + @Test + fun `on ShowShareSheet should call shareText on IntentHandler`() { + val text = "sharable stuff" + mutableEventFlow.tryEmit(SendEvent.ShowShareSheet(text)) + verify { + intentHandler.shareText(text) + } + } + @Test fun `on overflow item click should display menu`() { composeTestRule @@ -474,10 +484,27 @@ class SendScreenTest : BaseComposeTest() { .performClick() composeTestRule.assertNoDialogExists() } + + @Test + fun `loading dialog should be displayed according to state`() { + val loadingMessage = "syncing" + composeTestRule.onNode(isDialog()).assertDoesNotExist() + composeTestRule.onNodeWithText(loadingMessage).assertDoesNotExist() + + mutableStateFlow.update { + it.copy(dialogState = SendState.DialogState.Loading(loadingMessage.asText())) + } + + composeTestRule + .onNodeWithText(loadingMessage) + .assertIsDisplayed() + .assert(hasAnyAncestor(isDialog())) + } } private val DEFAULT_STATE: SendState = SendState( viewState = SendState.ViewState.Loading, + dialogState = null, ) private val DEFAULT_SEND_ITEM: SendState.ViewState.Content.SendItem = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt index 127823d02f..8fd1bef1b1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt @@ -114,6 +114,12 @@ class SendViewModelTest : BaseViewModelTest() { viewModel.trySendAction(SendAction.SyncClick) + assertEquals( + DEFAULT_STATE.copy( + dialogState = SendState.DialogState.Loading(R.string.syncing.asText()), + ), + viewModel.stateFlow.value, + ) verify { vaultRepo.sync() } @@ -146,12 +152,15 @@ class SendViewModelTest : BaseViewModelTest() { } @Test - fun `ShareClick should emit ShowToast`() = runTest { + fun `ShareClick should emit ShowShareSheet`() = runTest { val viewModel = createViewModel() - val sendItem = mockk() + val testUrl = "www.test.com" + val sendItem = mockk { + every { shareUrl } returns testUrl + } viewModel.eventFlow.test { viewModel.trySendAction(SendAction.ShareClick(sendItem)) - assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem()) + assertEquals(SendEvent.ShowShareSheet(testUrl), awaitItem()) } } @@ -175,7 +184,8 @@ class SendViewModelTest : BaseViewModelTest() { @Test fun `VaultRepository SendData Error should update view state to Error`() { - val viewModel = createViewModel() + val dialogState = SendState.DialogState.Loading(R.string.syncing.asText()) + val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) mutableSendDataFlow.value = DataState.Error(Throwable("Fail")) @@ -184,6 +194,7 @@ class SendViewModelTest : BaseViewModelTest() { viewState = SendState.ViewState.Error( message = R.string.generic_error_message.asText(), ), + dialogState = null, ), viewModel.stateFlow.value, ) @@ -191,7 +202,8 @@ class SendViewModelTest : BaseViewModelTest() { @Test fun `VaultRepository SendData Loaded should update view state`() { - val viewModel = createViewModel() + val dialogState = SendState.DialogState.Loading(R.string.syncing.asText()) + val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) val viewState = mockk() val sendData = mockk { every { @@ -201,24 +213,29 @@ class SendViewModelTest : BaseViewModelTest() { mutableSendDataFlow.value = DataState.Loaded(sendData) - assertEquals(SendState(viewState = viewState), viewModel.stateFlow.value) + assertEquals( + SendState(viewState = viewState, dialogState = null), + viewModel.stateFlow.value, + ) } @Test fun `VaultRepository SendData Loading should update view state to Loading`() { - val viewModel = createViewModel() + val dialogState = SendState.DialogState.Loading(R.string.syncing.asText()) + val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) mutableSendDataFlow.value = DataState.Loading assertEquals( - SendState(viewState = SendState.ViewState.Loading), + SendState(viewState = SendState.ViewState.Loading, dialogState = dialogState), viewModel.stateFlow.value, ) } @Test fun `VaultRepository SendData NoNetwork should update view state to Error`() { - val viewModel = createViewModel() + val dialogState = SendState.DialogState.Loading(R.string.syncing.asText()) + val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) mutableSendDataFlow.value = DataState.NoNetwork() @@ -229,6 +246,7 @@ class SendViewModelTest : BaseViewModelTest() { .asText() .concat(R.string.internet_connection_required_message.asText()), ), + dialogState = null, ), viewModel.stateFlow.value, ) @@ -236,7 +254,8 @@ class SendViewModelTest : BaseViewModelTest() { @Test fun `VaultRepository SendData Pending should update view state`() { - val viewModel = createViewModel() + val dialogState = SendState.DialogState.Loading(R.string.syncing.asText()) + val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) val viewState = mockk() val sendData = mockk { every { @@ -246,7 +265,10 @@ class SendViewModelTest : BaseViewModelTest() { mutableSendDataFlow.value = DataState.Pending(sendData) - assertEquals(SendState(viewState = viewState), viewModel.stateFlow.value) + assertEquals( + SendState(viewState = viewState, dialogState = dialogState), + viewModel.stateFlow.value, + ) } private fun createViewModel( @@ -269,4 +291,5 @@ private const val SEND_DATA_EXTENSIONS_PATH: String = private val DEFAULT_STATE: SendState = SendState( viewState = SendState.ViewState.Loading, + dialogState = null, )