Adding navigation for generator modals (#627)

This commit is contained in:
Joshua Queen 2024-01-15 18:35:03 -05:00 committed by Álison Fernandes
parent 61a162b6de
commit cd4db46e13
12 changed files with 153 additions and 7 deletions

View file

@ -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() })
}
}

View file

@ -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
}
/**

View file

@ -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.

View file

@ -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<String>(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,
)
}

View file

@ -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 = {},
)
}
}

View file

@ -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 {

View file

@ -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()
}
}

View file

@ -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,
)
}
}

View file

@ -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()
}

View file

@ -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()
}
/**

View file

@ -53,6 +53,7 @@ class GeneratorScreenTest : BaseComposeTest() {
GeneratorScreen(
viewModel = viewModel,
onNavigateToPasswordHistory = { onNavigateToPasswordHistoryScreenCalled = true },
onNavigateBack = {},
)
}
}

View file

@ -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<VaultAddEditEvent>()
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