diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index 120ed1d899..1c79f3d469 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -10,6 +10,8 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.folders.foldersDestinati import com.x8bit.bitwarden.ui.platform.feature.settings.folders.navigateToFolders import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination +import com.x8bit.bitwarden.ui.tools.feature.generator.generatorModalDestination +import com.x8bit.bitwarden.ui.tools.feature.generator.navigateToGeneratorModal import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.navigateToPasswordHistory import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.passwordHistoryDestination import com.x8bit.bitwarden.ui.tools.feature.send.addsend.addSendDestination @@ -70,6 +72,7 @@ fun NavGraphBuilder.vaultUnlockedGraph( navController.navigateToManualCodeEntryScreen() }, onNavigateBack = { navController.popBackStack() }, + onNavigateToGeneratorModal = { navController.navigateToGeneratorModal(mode = it) }, ) vaultMoveToOrganizationDestination( onNavigateBack = { navController.popBackStack() }, @@ -90,7 +93,6 @@ fun NavGraphBuilder.vaultUnlockedGraph( }, onNavigateBack = { navController.popBackStack() }, ) - vaultManualCodeEntryDestination( onNavigateToQrCodeScreen = { navController.popBackStack() @@ -102,5 +104,6 @@ fun NavGraphBuilder.vaultUnlockedGraph( addSendDestination(onNavigateBack = { navController.popBackStack() }) passwordHistoryDestination(onNavigateBack = { navController.popBackStack() }) foldersDestination(onNavigateBack = { navController.popBackStack() }) + generatorModalDestination(onNavigateBack = { navController.popBackStack() }) } } 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 b7a4aa389e..d15d93d34f 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 @@ -50,7 +50,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.SETTINGS_GRAPH_ROUTE import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToSettingsGraph import com.x8bit.bitwarden.ui.platform.feature.settings.settingsGraph import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders -import com.x8bit.bitwarden.ui.tools.feature.generator.GENERATOR_ROUTE +import com.x8bit.bitwarden.ui.tools.feature.generator.GENERATOR_GRAPH_ROUTE import com.x8bit.bitwarden.ui.tools.feature.generator.generatorGraph import com.x8bit.bitwarden.ui.tools.feature.generator.navigateToGeneratorGraph import com.x8bit.bitwarden.ui.tools.feature.send.SEND_GRAPH_ROUTE @@ -345,7 +345,7 @@ private sealed class VaultUnlockedNavBarTab : Parcelable { override val iconRes get() = R.drawable.ic_generator override val labelRes get() = R.string.generator override val contentDescriptionRes get() = R.string.generator - override val route get() = GENERATOR_ROUTE + override val route get() = GENERATOR_GRAPH_ROUTE } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorGraphNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorGraphNavigation.kt index 808610d36c..a0d5a29ae7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorGraphNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorGraphNavigation.kt @@ -5,7 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.navigation -private const val GENERATOR_GRAPH_ROUTE: String = "generator_graph" +const val GENERATOR_GRAPH_ROUTE: String = "generator_graph" /** * Add generator destination to the root nav graph. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt index 07c4b5c31c..c339dea5f5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt @@ -1,20 +1,42 @@ package com.x8bit.bitwarden.ui.tools.feature.generator +import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions +import androidx.navigation.NavType import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage +import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode /** * The functions below pertain to entry into the [GeneratorScreen]. */ +private const val GENERATOR_MODAL_ROUTE_PREFIX: String = "generator_modal" +private const val GENERATOR_MODE_TYPE: String = "generator_mode_type" +private const val USERNAME_GENERATOR: String = "username_generator" +private const val PASSWORD_GENERATOR: String = "password_generator" + const val GENERATOR_ROUTE: String = "generator" +private const val GENERATOR_MODAL_ROUTE: String = + "$GENERATOR_MODAL_ROUTE_PREFIX/{$GENERATOR_MODE_TYPE}" /** - * Navigate to the [GeneratorScreen]. + * Class to retrieve vault item listing arguments from the [SavedStateHandle]. */ -fun NavController.navigateToGenerator(navOptions: NavOptions? = null) { - navigate(GENERATOR_ROUTE, navOptions) +@OmitFromCoverage +data class GeneratorArgs( + val type: GeneratorMode, +) { + constructor(savedStateHandle: SavedStateHandle) : this( + type = when (savedStateHandle.get(GENERATOR_MODE_TYPE)) { + USERNAME_GENERATOR -> GeneratorMode.Modal.Username + PASSWORD_GENERATOR -> GeneratorMode.Modal.Password + else -> GeneratorMode.Default + }, + ) } /** @@ -26,6 +48,43 @@ fun NavGraphBuilder.generatorDestination( composable(GENERATOR_ROUTE) { GeneratorScreen( onNavigateToPasswordHistory = onNavigateToPasswordHistory, + onNavigateBack = {}, ) } } + +/** + * Add the generator modal destination to the nav graph. + */ +fun NavGraphBuilder.generatorModalDestination( + onNavigateBack: () -> Unit, +) { + composableWithSlideTransitions( + route = GENERATOR_MODAL_ROUTE, + arguments = listOf( + navArgument(GENERATOR_MODE_TYPE) { type = NavType.StringType }, + ), + ) { + GeneratorScreen( + onNavigateToPasswordHistory = {}, + onNavigateBack = onNavigateBack, + ) + } +} + +/** + * Navigate to the generator screen in the username generation mode. + */ +fun NavController.navigateToGeneratorModal( + mode: GeneratorMode.Modal, + navOptions: NavOptions? = null, +) { + val generatorModeType = when (mode) { + GeneratorMode.Modal.Password -> PASSWORD_GENERATOR + GeneratorMode.Modal.Username -> USERNAME_GENERATOR + } + navigate( + route = "$GENERATOR_MODAL_ROUTE_PREFIX/$generatorModeType", + navOptions = navOptions, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt index cef1865048..d56f043a94 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt @@ -84,6 +84,7 @@ import kotlinx.collections.immutable.toImmutableList fun GeneratorScreen( viewModel: GeneratorViewModel = hiltViewModel(), onNavigateToPasswordHistory: () -> Unit, + onNavigateBack: () -> Unit, ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() val context = LocalContext.current @@ -1059,6 +1060,7 @@ private fun GeneratorPreview() { BitwardenTheme { GeneratorScreen( onNavigateToPasswordHistory = {}, + onNavigateBack = {}, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt index e4f45bedd1..9ce1d90e36 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt @@ -39,6 +39,7 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Us import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.SimpleLogin import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.RandomWord +import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import com.x8bit.bitwarden.ui.tools.feature.generator.util.toUsernameGeneratorRequest import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -50,6 +51,7 @@ import kotlinx.parcelize.Parcelize import javax.inject.Inject private const val KEY_STATE = "state" +private const val KEY_GENERATOR_MODE = "key_generator_mode" /** * ViewModel responsible for handling user interactions in the generator screen. @@ -73,6 +75,7 @@ class GeneratorViewModel @Inject constructor( selectedType = Passcode( selectedType = Password(), ), + generatorMode = GeneratorArgs(savedStateHandle).type, currentEmailAddress = requireNotNull(authRepository.userStateFlow.value?.activeAccount?.email), ), @@ -1409,6 +1412,7 @@ class GeneratorViewModel @Inject constructor( data class GeneratorState( val generatedText: String, val selectedType: MainType, + val generatorMode: GeneratorMode = GeneratorMode.Default, val currentEmailAddress: String, ) : Parcelable { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/model/GeneratorMode.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/model/GeneratorMode.kt new file mode 100644 index 0000000000..195a25c278 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/model/GeneratorMode.kt @@ -0,0 +1,34 @@ +package com.x8bit.bitwarden.ui.tools.feature.generator.model + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +/** + * A sealed class representing the mode in which the generator displays. + */ +sealed class GeneratorMode : Parcelable { + /** + * Represents the main or default generator mode. + */ + @Parcelize + data object Default : GeneratorMode() + + /** + * A sealed class representing the types of modals in which the generator displays. + */ + @Parcelize + sealed class Modal : GeneratorMode() { + + /** + * Represents the mode for generating passwords. + */ + @Parcelize + data object Password : Modal() + + /** + * Represents the mode for generating usernames. + */ + @Parcelize + data object Username : Modal() + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt index 99b6e720f6..7e8b5d72e4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt @@ -8,6 +8,7 @@ import androidx.navigation.NavType import androidx.navigation.navArgument import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType private const val ADD_TYPE: String = "add" @@ -43,6 +44,7 @@ fun NavGraphBuilder.vaultAddEditDestination( onNavigateBack: () -> Unit, onNavigateToManualCodeEntryScreen: () -> Unit, onNavigateToQrCodeScanScreen: () -> Unit, + onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit, ) { composableWithSlideTransitions( route = ADD_EDIT_ITEM_ROUTE, @@ -54,6 +56,7 @@ fun NavGraphBuilder.vaultAddEditDestination( onNavigateBack = onNavigateBack, onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen, onNavigateToQrCodeScanScreen = onNavigateToQrCodeScanScreen, + onNavigateToGeneratorModal = onNavigateToGeneratorModal, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt index c77e042010..48887b2044 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt @@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState +import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers @@ -50,6 +51,7 @@ fun VaultAddEditScreen( permissionsManager: PermissionsManager = PermissionsManagerImpl(LocalContext.current as Activity), onNavigateToManualCodeEntryScreen: () -> Unit, + onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit, ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() val context = LocalContext.current @@ -65,6 +67,10 @@ fun VaultAddEditScreen( onNavigateToManualCodeEntryScreen() } + is VaultAddEditEvent.NavigateToGeneratorModal -> { + onNavigateToGeneratorModal(event.generatorMode) + } + is VaultAddEditEvent.ShowToast -> { Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index e5e087b3a0..3603ce2b13 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -16,6 +16,7 @@ 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.platform.base.util.concat +import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState @@ -1316,6 +1317,13 @@ sealed class VaultAddEditEvent { * Navigate to the manual code entry screen. */ data object NavigateToManualCodeEntry : VaultAddEditEvent() + + /** + * Navigate to the generator modal. + */ + data class NavigateToGeneratorModal( + val generatorMode: GeneratorMode.Modal, + ) : VaultAddEditEvent() } /** diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt index 139576c345..9fb3847e2b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt @@ -53,6 +53,7 @@ class GeneratorScreenTest : BaseComposeTest() { GeneratorScreen( viewModel = viewModel, onNavigateToPasswordHistory = { onNavigateToPasswordHistoryScreenCalled = true }, + onNavigateBack = {}, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt index 8ac44de2d7..3c988bcc5e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt @@ -31,6 +31,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.util.FakePermissionManager import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import com.x8bit.bitwarden.ui.util.isProgressBar import com.x8bit.bitwarden.ui.util.onAllNodesWithContentDescriptionAfterScroll import com.x8bit.bitwarden.ui.util.onAllNodesWithTextAfterScroll @@ -46,6 +47,7 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -56,6 +58,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { private var onNavigateBackCalled = false private var onNavigateQrCodeScanScreenCalled = false private var onNavigateToManualCodeEntryScreenCalled = false + private var onNavigateToGeneratorModalType: GeneratorMode.Modal? = null private val mutableEventFlow = bufferedMutableSharedFlow() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE_LOGIN) @@ -78,6 +81,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { onNavigateToManualCodeEntryScreen = { onNavigateToManualCodeEntryScreenCalled = true }, + onNavigateToGeneratorModal = { onNavigateToGeneratorModalType = it }, viewModel = viewModel, permissionsManager = fakePermissionManager, ) @@ -104,6 +108,28 @@ class VaultAddEditScreenTest : BaseComposeTest() { assertTrue(onNavigateToManualCodeEntryScreenCalled) } + @Suppress("MaxLineLength") + @Test + fun `on NavigateToGeneratorModal event in password mode should invoke NavigateToGeneratorModal with Password Generator Mode `() { + mutableEventFlow.tryEmit( + VaultAddEditEvent.NavigateToGeneratorModal( + generatorMode = GeneratorMode.Modal.Password, + ), + ) + assertEquals(GeneratorMode.Modal.Password, onNavigateToGeneratorModalType) + } + + @Suppress("MaxLineLength") + @Test + fun `on NavigateToGeneratorModal event in username mode should invoke NavigateToGeneratorModal with Username Generator Mode `() { + mutableEventFlow.tryEmit( + VaultAddEditEvent.NavigateToGeneratorModal( + generatorMode = GeneratorMode.Modal.Username, + ), + ) + assertEquals(GeneratorMode.Modal.Username, onNavigateToGeneratorModalType) + } + @Test fun `clicking close button should send CloseClick action`() { composeTestRule