diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt index e433e4edf5..c9d53fef83 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt @@ -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() }) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt index 169377a0a1..5657d25f52 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt @@ -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, ) } } 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 18fd6c12bf..caaba598aa 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 @@ -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(), + ) + } } } } 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 f1f3906439..e921fc1361 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,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( 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() + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index e82b78ec3c..7b01a79d8b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -246,6 +246,7 @@ private fun VaultUnlockedNavBarScaffold( onNavigateToPendingRequests = navigateToPendingRequests, onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen, onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen, + onNavigateToImportLogins = onNavigateToImportLogins, ) } } 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 1bb798f5ab..3857b499fa 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 @@ -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()) } } } 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 619b05e30e..90ee0d7736 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,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 { + 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, ) }