PM-13609 Navigate to new import flow from Vault settings when feature is enabled. (#4090)

This commit is contained in:
Dave Severns 2024-10-15 16:12:29 -04:00 committed by GitHub
parent 736912bd6c
commit 970a1e14cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 136 additions and 20 deletions

View file

@ -35,6 +35,7 @@ fun NavGraphBuilder.settingsGraph(
onNavigateToPendingRequests: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToImportLogins: () -> Unit,
) {
navigation(
startDestination = SETTINGS_ROUTE,
@ -70,6 +71,7 @@ fun NavGraphBuilder.settingsGraph(
onNavigateBack = { navController.popBackStack() },
onNavigateToExportVault = onNavigateToExportVault,
onNavigateToFolders = onNavigateToFolders,
onNavigateToImportLogins = onNavigateToImportLogins,
)
blockAutoFillDestination(onNavigateBack = { navController.popBackStack() })
}

View file

@ -14,6 +14,7 @@ fun NavGraphBuilder.vaultSettingsDestination(
onNavigateBack: () -> Unit,
onNavigateToExportVault: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToImportLogins: () -> Unit,
) {
composableWithPushTransitions(
route = VAULT_SETTINGS_ROUTE,
@ -22,6 +23,7 @@ fun NavGraphBuilder.vaultSettingsDestination(
onNavigateBack = onNavigateBack,
onNavigateToExportVault = onNavigateToExportVault,
onNavigateToFolders = onNavigateToFolders,
onNavigateToImportLogins = onNavigateToImportLogins,
)
}
}

View file

@ -11,6 +11,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -40,10 +41,11 @@ fun VaultSettingsScreen(
onNavigateBack: () -> Unit,
onNavigateToExportVault: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToImportLogins: () -> Unit,
viewModel: VaultSettingsViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
) {
val state = viewModel.stateFlow.collectAsStateWithLifecycle()
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
EventsEffect(viewModel = viewModel) { event ->
@ -56,7 +58,11 @@ fun VaultSettingsScreen(
}
is VaultSettingsEvent.NavigateToImportVault -> {
intentManager.launchUri(event.url.toUri())
if (state.isNewImportLoginsFlowEnabled) {
onNavigateToImportLogins()
} else {
intentManager.launchUri(event.url.toUri())
}
}
}
}
@ -106,22 +112,35 @@ fun VaultSettingsScreen(
.fillMaxWidth(),
)
BitwardenExternalLinkRow(
text = stringResource(R.string.import_items),
onConfirmClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
},
withDivider = true,
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.importUrl,
),
modifier = Modifier
.testTag("ImportItemsLinkItemView")
.fillMaxWidth(),
)
if (state.isNewImportLoginsFlowEnabled) {
BitwardenTextRow(
text = stringResource(R.string.import_items),
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
},
withDivider = true,
modifier = Modifier
.testTag("ImportItemsLinkItemView")
.fillMaxWidth(),
)
} else {
BitwardenExternalLinkRow(
text = stringResource(R.string.import_items),
onConfirmClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
},
withDivider = true,
dialogTitle = stringResource(id = R.string.continue_to_web_app),
dialogMessage =
stringResource(
id = R.string.you_can_import_data_to_your_vault_on_x,
state.importUrl,
),
modifier = Modifier
.testTag("ImportItemsLinkItemView")
.fillMaxWidth(),
)
}
}
}
}

View file

@ -1,9 +1,16 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.util.toBaseWebVaultImportUrl
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import javax.inject.Inject
/**
@ -11,7 +18,8 @@ import javax.inject.Inject
*/
@HiltViewModel
class VaultSettingsViewModel @Inject constructor(
val environmentRepository: EnvironmentRepository,
environmentRepository: EnvironmentRepository,
val featureFlagManager: FeatureFlagManager,
) : BaseViewModel<VaultSettingsState, VaultSettingsEvent, VaultSettingsAction>(
initialState = run {
VaultSettingsState(
@ -19,15 +27,35 @@ class VaultSettingsViewModel @Inject constructor(
.environment
.environmentUrlData
.toBaseWebVaultImportUrl,
isNewImportLoginsFlowEnabled = featureFlagManager
.getFeatureFlag(FlagKey.ImportLoginsFlow),
)
},
) {
init {
featureFlagManager
.getFeatureFlagFlow(FlagKey.ImportLoginsFlow)
.map { VaultSettingsAction.Internal.ImportLoginsFeatureFlagChanged(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: VaultSettingsAction): Unit = when (action) {
VaultSettingsAction.BackClick -> handleBackClicked()
VaultSettingsAction.ExportVaultClick -> handleExportVaultClicked()
VaultSettingsAction.FoldersButtonClick -> handleFoldersButtonClicked()
VaultSettingsAction.ImportItemsClick -> handleImportItemsClicked()
is VaultSettingsAction.Internal.ImportLoginsFeatureFlagChanged -> {
handleImportLoginsFeatureFlagChanged(action)
}
}
private fun handleImportLoginsFeatureFlagChanged(
action: VaultSettingsAction.Internal.ImportLoginsFeatureFlagChanged,
) {
mutableStateFlow.update {
it.copy(isNewImportLoginsFlowEnabled = action.isEnabled)
}
}
private fun handleBackClicked() {
@ -54,6 +82,7 @@ class VaultSettingsViewModel @Inject constructor(
*/
data class VaultSettingsState(
val importUrl: String,
val isNewImportLoginsFlowEnabled: Boolean,
)
/**
@ -111,4 +140,17 @@ sealed class VaultSettingsAction {
* Indicates that the user clicked the Import Items button.
*/
data object ImportItemsClick : VaultSettingsAction()
/**
* Internal actions not performed by user interation
*/
sealed class Internal : VaultSettingsAction() {
/**
* Indicates that the import logins feature flag has changed.
*/
data class ImportLoginsFeatureFlagChanged(
val isEnabled: Boolean,
) : Internal()
}
}

View file

@ -246,6 +246,7 @@ private fun VaultUnlockedNavBarScaffold(
onNavigateToPendingRequests = navigateToPendingRequests,
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
onNavigateToImportLogins = onNavigateToImportLogins,
)
}
}

View file

@ -17,12 +17,15 @@ import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class VaultSettingsScreenTest : BaseComposeTest() {
private var onNavigateToImportLoginsCalled = false
private var onNavigateBackCalled = false
private var onNavigateToExportVaultCalled = false
private var onNavigateToFoldersCalled = false
@ -30,6 +33,7 @@ class VaultSettingsScreenTest : BaseComposeTest() {
private val mutableStateFlow = MutableStateFlow(
VaultSettingsState(
importUrl = "testUrl/#/tools/import",
isNewImportLoginsFlowEnabled = false,
),
)
private val intentManager: IntentManager = mockk(relaxed = true) {
@ -50,6 +54,7 @@ class VaultSettingsScreenTest : BaseComposeTest() {
onNavigateToExportVault = { onNavigateToExportVaultCalled = true },
onNavigateToFolders = { onNavigateToFoldersCalled = true },
intentManager = intentManager,
onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true },
)
}
}
@ -124,11 +129,32 @@ class VaultSettingsScreenTest : BaseComposeTest() {
}
@Test
fun `on NavigateToImportVault should invoke IntentManager`() {
fun `on NavigateToImportVault should invoke IntentManager not lambda`() {
val testUrl = "testUrl"
mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateToImportVault(testUrl))
verify {
intentManager.launchUri(testUrl.toUri())
}
assertFalse(onNavigateToImportLoginsCalled)
}
@Test
fun `when new logins feature flag is enabled send action right when import items is clicked`() {
mutableStateFlow.update {
it.copy(isNewImportLoginsFlowEnabled = true)
}
composeTestRule.onNodeWithText("Import items").performClick()
verify { viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
}
@Test
fun `when new logins feature flag is enabled NavigateToImportVault should invoke lambda`() {
mutableStateFlow.update {
it.copy(isNewImportLoginsFlowEnabled = true)
}
val testUrl = "testUrl"
mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateToImportVault(testUrl))
assertTrue(onNavigateToImportLoginsCalled)
verify(exactly = 0) { intentManager.launchUri(testUrl.toUri()) }
}
}

View file

@ -1,14 +1,27 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import app.cash.turbine.test
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class VaultSettingsViewModelTest : BaseViewModelTest() {
private val environmentRepository = FakeEnvironmentRepository()
private val mutableImportLoginsFlagFlow = MutableStateFlow(false)
private val featureFlagManager = mockk<FeatureFlagManager> {
every { getFeatureFlagFlow(FlagKey.ImportLoginsFlow) } returns mutableImportLoginsFlagFlow
every { getFeatureFlag(FlagKey.ImportLoginsFlow) } returns false
}
@Test
fun `BackClick should emit NavigateBack`() = runTest {
@ -44,7 +57,18 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `ImportLoginsFeatureFlagChanged should update state`() {
val viewModel = createViewModel()
assertFalse(
viewModel.stateFlow.value.isNewImportLoginsFlowEnabled,
)
mutableImportLoginsFlagFlow.update { true }
assertTrue(viewModel.stateFlow.value.isNewImportLoginsFlowEnabled)
}
private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel(
environmentRepository = environmentRepository,
featureFlagManager = featureFlagManager,
)
}