From 77913805ab1ccb21f579b90fd3ac123cebf3d9d3 Mon Sep 17 00:00:00 2001 From: Oleg Semenenko <146032743+oleg-livefront@users.noreply.github.com> Date: Wed, 31 Jan 2024 20:50:45 -0600 Subject: [PATCH] BIT-972 Add the import url (#903) --- .../util/EnvironmentUrlDataJsonExtensions.kt | 9 ++++ .../settings/vault/VaultSettingsScreen.kt | 19 ++++++++- .../settings/vault/VaultSettingsViewModel.kt | 42 ++++++++++++++++--- .../EnvironmentUrlsDataJsonExtensionsTest.kt | 23 ++++++++++ .../settings/vault/VaultSettingsScreenTest.kt | 22 +++++++++- .../vault/VaultSettingsViewModelTest.kt | 11 +++-- 6 files changed, 114 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentUrlDataJsonExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentUrlDataJsonExtensions.kt index 9c33ca1611..9c97160515 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentUrlDataJsonExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentUrlDataJsonExtensions.kt @@ -50,6 +50,15 @@ val EnvironmentUrlDataJson.baseWebSendUrl: String ?.let { "$it/#/send/" } ?: DEFAULT_WEB_SEND_URL +/** + * Returns the base web vault import URL or the default value if one is not present. + */ +val EnvironmentUrlDataJson.toBaseWebVaultImportUrl: String + get() = + this + .baseWebVaultUrlOrDefault + .let { "$it/#/tools/import" } + /** * Returns a base icon url based on the environment or the default value if values are missing. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt index 7381626c81..884a29d422 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt @@ -17,13 +17,17 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.core.net.toUri 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.components.BitwardenExternalLinkRow import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar +import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager +import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager /** * Displays the vault settings screen. @@ -36,7 +40,10 @@ fun VaultSettingsScreen( onNavigateToExportVault: () -> Unit, onNavigateToFolders: () -> Unit, viewModel: VaultSettingsViewModel = hiltViewModel(), + intentManager: IntentManager = LocalIntentManager.current, ) { + val state = viewModel.stateFlow.collectAsStateWithLifecycle() + val context = LocalContext.current EventsEffect(viewModel = viewModel) { event -> when (event) { @@ -46,6 +53,10 @@ fun VaultSettingsScreen( is VaultSettingsEvent.ShowToast -> { Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() } + + is VaultSettingsEvent.NavigateToImportVault -> { + intentManager.launchUri(event.url.toUri()) + } } } @@ -96,8 +107,12 @@ fun VaultSettingsScreen( { viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) } }, withDivider = true, - dialogTitle = stringResource(id = R.string.import_items_confirmation), - dialogMessage = stringResource(id = R.string.import_items_description), + dialogTitle = stringResource(id = R.string.continue_to_web_app), + dialogMessage = + stringResource( + id = R.string.you_can_import_data_to_your_vault_on_x, + state.value.baseUrl, + ), modifier = Modifier.fillMaxWidth(), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt index 0a0d1866ae..5e7ed853bf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt @@ -1,5 +1,8 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault +import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository +import com.x8bit.bitwarden.data.platform.repository.util.baseWebVaultUrlOrDefault +import com.x8bit.bitwarden.data.platform.repository.util.toBaseWebVaultImportUrl import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -8,10 +11,23 @@ import javax.inject.Inject * View model for the vault screen. */ @HiltViewModel -class VaultSettingsViewModel @Inject constructor() : - BaseViewModel( - initialState = Unit, - ) { +class VaultSettingsViewModel @Inject constructor( + val environmentRepository: EnvironmentRepository, +) : BaseViewModel( + initialState = run { + VaultSettingsState( + baseUrl = environmentRepository + .environment + .environmentUrlData + .baseWebVaultUrlOrDefault, + importUrl = environmentRepository + .environment + .environmentUrlData + .toBaseWebVaultImportUrl, + ) + }, +) { + override fun handleAction(action: VaultSettingsAction): Unit = when (action) { VaultSettingsAction.BackClick -> handleBackClicked() VaultSettingsAction.ExportVaultClick -> handleExportVaultClicked() @@ -32,11 +48,20 @@ class VaultSettingsViewModel @Inject constructor() : } private fun handleImportItemsClicked() { - // TODO BIT-972 implement import items functionality - sendEvent(VaultSettingsEvent.ShowToast("Not yet implemented.")) + sendEvent( + VaultSettingsEvent.NavigateToImportVault(state.importUrl), + ) } } +/** + * Models the state for the VaultSettingScreen. + */ +data class VaultSettingsState( + val baseUrl: String, + val importUrl: String, +) + /** * Models events for the vault screen. */ @@ -46,6 +71,11 @@ sealed class VaultSettingsEvent { */ data object NavigateBack : VaultSettingsEvent() + /** + * Navigate to the import vault URL. + */ + data class NavigateToImportVault(val url: String) : VaultSettingsEvent() + /** * Navigate to the Export Vault screen. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt index 4d8eabdf1d..b88a4f9b57 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt @@ -236,6 +236,29 @@ class EnvironmentUrlsDataJsonExtensionsTest { .baseIconUrl, ) } + + @Test + fun `toBaseWebVaultImportUrl should return correct url if webVault is empty`() { + val expectedUrl = "base/#/tools/import" + + assertEquals( + expectedUrl, + DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.copy( + webVault = null, + ) + .toBaseWebVaultImportUrl, + ) + } + + @Test + fun `toBaseWebVaultImportUrl should correctly convert to the import url`() { + val expectedUrl = "webVault/#/tools/import" + + assertEquals( + expectedUrl, + DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.toBaseWebVaultImportUrl, + ) + } } private val DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA = EnvironmentUrlDataJson( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt index bc1af3bcc7..a8e6a69e2b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt @@ -7,8 +7,10 @@ import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.core.net.toUri import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest +import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -23,7 +25,15 @@ class VaultSettingsScreenTest : BaseComposeTest() { private var onNavigateToExportVaultCalled = false private var onNavigateToFoldersCalled = false private val mutableEventFlow = bufferedMutableSharedFlow() - private val mutableStateFlow = MutableStateFlow(Unit) + private val mutableStateFlow = MutableStateFlow( + VaultSettingsState( + baseUrl = "testUrl", importUrl = "testUrl/#/tools/import", + ), + ) + private val intentManager: IntentManager = mockk(relaxed = true) { + every { launchUri(any()) } returns Unit + } + val viewModel = mockk(relaxed = true) { every { eventFlow } returns mutableEventFlow every { stateFlow } returns mutableStateFlow @@ -37,6 +47,7 @@ class VaultSettingsScreenTest : BaseComposeTest() { onNavigateBack = { onNavigateBackCalled = true }, onNavigateToExportVault = { onNavigateToExportVaultCalled = true }, onNavigateToFolders = { onNavigateToFoldersCalled = true }, + intentManager = intentManager, ) } } @@ -101,4 +112,13 @@ class VaultSettingsScreenTest : BaseComposeTest() { mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateToFolders) assertTrue(onNavigateToFoldersCalled) } + + @Test + fun `on NavigateToImportVault should invoke IntentManager`() { + val testUrl = "testUrl" + mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateToImportVault(testUrl)) + verify { + intentManager.launchUri(testUrl.toUri()) + } + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt index 716b645646..619b05e30e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt @@ -1,12 +1,14 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault import app.cash.turbine.test +import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class VaultSettingsViewModelTest : BaseViewModelTest() { + private val environmentRepository = FakeEnvironmentRepository() @Test fun `BackClick should emit NavigateBack`() = runTest { @@ -30,16 +32,19 @@ class VaultSettingsViewModelTest : BaseViewModelTest() { } @Test - fun `ImportItemsClick should emit ShowToast`() = runTest { + fun `ImportItemsClick should emit send NavigateToImportVault with correct url`() = runTest { val viewModel = createViewModel() + val expected = "https://vault.bitwarden.com/#/tools/import" viewModel.eventFlow.test { viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) assertEquals( - VaultSettingsEvent.ShowToast("Not yet implemented."), + VaultSettingsEvent.NavigateToImportVault(expected), awaitItem(), ) } } - private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel() + private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel( + environmentRepository = environmentRepository, + ) }