Add navigation support for editing a send (#558)

This commit is contained in:
David Perez 2024-01-09 20:35:46 -06:00 committed by Álison Fernandes
parent 49411f3e2f
commit 8d45a650c3
16 changed files with 184 additions and 38 deletions

View file

@ -13,6 +13,7 @@ import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlocked
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
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend
import com.x8bit.bitwarden.ui.vault.feature.addedit.navigateToVaultAddEdit
import com.x8bit.bitwarden.ui.vault.feature.addedit.vaultAddEditDestination
@ -52,7 +53,8 @@ fun NavGraphBuilder.vaultUnlockedGraph(
onNavigateToVaultEditItem = {
navController.navigateToVaultAddEdit(VaultAddEditType.EditItem(it))
},
onNavigateToAddSend = { navController.navigateToAddSend() },
onNavigateToAddSend = { navController.navigateToAddSend(AddSendType.AddItem) },
onNavigateToEditSend = { navController.navigateToAddSend(AddSendType.EditItem(it)) },
onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() },
onNavigateToPasswordHistory = { navController.navigateToPasswordHistory() },
)

View file

@ -26,6 +26,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToVaultItem: (vaultItemId: String) -> Unit,
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
onNavigateToAddSend: () -> Unit,
onNavigateToEditSend: (sendItemId: String) -> Unit,
onNavigateToDeleteAccount: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToPasswordHistory: () -> Unit,
@ -38,6 +39,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToVaultItem = onNavigateToVaultItem,
onNavigateToVaultEditItem = onNavigateToVaultEditItem,
onNavigateToAddSend = onNavigateToAddSend,
onNavigateToEditSend = onNavigateToEditSend,
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
onNavigateToFolders = onNavigateToFolders,
onNavigateToPasswordHistory = onNavigateToPasswordHistory,

View file

@ -71,6 +71,7 @@ fun VaultUnlockedNavBarScreen(
onNavigateToVaultItem: (vaultItemId: String) -> Unit,
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
onNavigateToAddSend: () -> Unit,
onNavigateToEditSend: (sendItemId: String) -> Unit,
onNavigateToDeleteAccount: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToPasswordHistory: () -> Unit,
@ -103,6 +104,7 @@ fun VaultUnlockedNavBarScreen(
onNavigateToVaultEditItem = onNavigateToVaultEditItem,
navigateToVaultAddItem = onNavigateToVaultAddItem,
navigateToAddSend = onNavigateToAddSend,
onNavigateToEditSend = onNavigateToEditSend,
navigateToDeleteAccount = onNavigateToDeleteAccount,
navigateToFolders = onNavigateToFolders,
navigateToPasswordHistory = onNavigateToPasswordHistory,
@ -136,6 +138,7 @@ private fun VaultUnlockedNavBarScaffold(
onNavigateToVaultItem: (vaultItemId: String) -> Unit,
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
navigateToAddSend: () -> Unit,
onNavigateToEditSend: (sendItemId: String) -> Unit,
navigateToDeleteAccount: () -> Unit,
navigateToFolders: () -> Unit,
navigateToPasswordHistory: () -> Unit,
@ -197,7 +200,10 @@ private fun VaultUnlockedNavBarScaffold(
shouldDimNavBar = shouldDim
},
)
sendGraph(onNavigateToAddSend = navigateToAddSend)
sendGraph(
onNavigateToAddSend = navigateToAddSend,
onNavigateToEditSend = onNavigateToEditSend,
)
generatorDestination(
onNavigateToPasswordHistory = { navigateToPasswordHistory() },
)

View file

@ -12,12 +12,16 @@ const val SEND_GRAPH_ROUTE: String = "send_graph"
*/
fun NavGraphBuilder.sendGraph(
onNavigateToAddSend: () -> Unit,
onNavigateToEditSend: (sendItemId: String) -> Unit,
) {
navigation(
startDestination = SEND_ROUTE,
route = SEND_GRAPH_ROUTE,
) {
sendDestination(onNavigateToAddSend = onNavigateToAddSend)
sendDestination(
onNavigateToAddSend = onNavigateToAddSend,
onNavigateToEditSend = onNavigateToEditSend,
)
}
}

View file

@ -12,12 +12,14 @@ const val SEND_ROUTE: String = "send"
*/
fun NavGraphBuilder.sendDestination(
onNavigateToAddSend: () -> Unit,
onNavigateToEditSend: (sendItemId: String) -> Unit,
) {
composableWithRootPushTransitions(
route = SEND_ROUTE,
) {
SendScreen(
onNavigateToAddSend = onNavigateToAddSend,
onNavigateToEditSend = onNavigateToEditSend,
)
}
}

View file

@ -47,6 +47,7 @@ import kotlinx.collections.immutable.persistentListOf
@Composable
fun SendScreen(
onNavigateToAddSend: () -> Unit,
onNavigateToEditSend: (sendItemId: String) -> Unit,
viewModel: SendViewModel = hiltViewModel(),
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current),
) {
@ -56,6 +57,8 @@ fun SendScreen(
when (event) {
is SendEvent.NavigateNewSend -> onNavigateToAddSend()
is SendEvent.NavigateToEditSend -> onNavigateToEditSend(event.sendId)
is SendEvent.NavigateToAboutSend -> {
intentHandler.launchUri("https://bitwarden.com/products/send".toUri())
}

View file

@ -169,8 +169,7 @@ class SendViewModel @Inject constructor(
}
private fun handleSendClick(action: SendAction.SendClick) {
// TODO: Navigate to the edit send screen (BIT-1387)
sendEvent(SendEvent.ShowToast("Not yet implemented".asText()))
sendEvent(SendEvent.NavigateToEditSend(action.sendItem.id))
}
private fun handleShareClick(action: SendAction.ShareClick) {
@ -368,6 +367,11 @@ sealed class SendEvent {
*/
data object NavigateNewSend : SendEvent()
/**
* Navigate to the edit send screen.
*/
data class NavigateToEditSend(val sendId: String) : SendEvent()
/**
* Navigate to the about send screen.
*/

View file

@ -1,11 +1,40 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
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.send.addsend.model.AddSendType
private const val ADD_SEND_ROUTE = "add_send"
private const val ADD_TYPE: String = "add"
private const val EDIT_TYPE: String = "edit"
private const val EDIT_ITEM_ID: String = "edit_send_id"
private const val ADD_SEND_ITEM_PREFIX: String = "add_send_item"
private const val ADD_SEND_ITEM_TYPE: String = "add_send_item_type"
private const val ADD_SEND_ROUTE: String =
"$ADD_SEND_ITEM_PREFIX/{$ADD_SEND_ITEM_TYPE}?$EDIT_ITEM_ID={$EDIT_ITEM_ID}"
/**
* Class to retrieve send add & edit arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class AddSendArgs(
val sendAddType: AddSendType,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
sendAddType = when (requireNotNull(savedStateHandle[ADD_SEND_ITEM_TYPE])) {
ADD_TYPE -> AddSendType.AddItem
EDIT_TYPE -> AddSendType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
else -> throw IllegalStateException("Unknown VaultAddEditType.")
},
)
}
/**
* Add the new send screen to the nav graph.
@ -15,6 +44,9 @@ fun NavGraphBuilder.addSendDestination(
) {
composableWithSlideTransitions(
route = ADD_SEND_ROUTE,
arguments = listOf(
navArgument(ADD_SEND_ITEM_TYPE) { type = NavType.StringType },
),
) {
AddSendScreen(onNavigateBack = onNavigateBack)
}
@ -23,6 +55,22 @@ fun NavGraphBuilder.addSendDestination(
/**
* Navigate to the new send screen.
*/
fun NavController.navigateToAddSend(navOptions: NavOptions? = null) {
navigate(ADD_SEND_ROUTE, navOptions)
fun NavController.navigateToAddSend(
sendAddType: AddSendType,
navOptions: NavOptions? = null,
) {
navigate(
route = "$ADD_SEND_ITEM_PREFIX/${sendAddType.toTypeString()}" +
"?${EDIT_ITEM_ID}=${sendAddType.toIdOrNull()}",
navOptions = navOptions,
)
}
private fun AddSendType.toTypeString(): String =
when (this) {
is AddSendType.AddItem -> ADD_TYPE
is AddSendType.EditItem -> EDIT_TYPE
}
private fun AddSendType.toIdOrNull(): String? =
(this as? AddSendType.EditItem)?.sendItemId

View file

@ -72,7 +72,7 @@ fun AddSendScreen(
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.add_send),
title = state.screenDisplayName(),
navigationIcon = painterResource(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {

View file

@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
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.tools.feature.send.addsend.model.AddSendType
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendView
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
import dagger.hilt.android.lifecycle.HiltViewModel
@ -41,31 +42,42 @@ class AddSendViewModel @Inject constructor(
private val environmentRepo: EnvironmentRepository,
private val vaultRepo: VaultRepository,
) : BaseViewModel<AddSendState, AddSendEvent, AddSendAction>(
initialState = savedStateHandle[KEY_STATE] ?: AddSendState(
viewState = AddSendState.ViewState.Content(
common = AddSendState.ViewState.Content.Common(
name = "",
maxAccessCount = null,
passwordInput = "",
noteInput = "",
isHideEmailChecked = false,
isDeactivateChecked = false,
deletionDate = ZonedDateTime
.now(clock)
// We want the default time to be midnight, so we remove all values beyond days
.truncatedTo(ChronoUnit.DAYS)
.plusWeeks(1),
expirationDate = null,
),
selectedType = AddSendState.ViewState.Content.SendType.Text(
input = "",
isHideByDefaultChecked = false,
),
),
dialogState = null,
isPremiumUser = authRepo.userStateFlow.value?.activeAccount?.isPremium == true,
baseWebSendUrl = environmentRepo.environment.environmentUrlData.baseWebSendUrl,
),
initialState = savedStateHandle[KEY_STATE] ?: run {
val addSendType = AddSendArgs(savedStateHandle).sendAddType
AddSendState(
addSendType = addSendType,
viewState = when (addSendType) {
AddSendType.AddItem -> AddSendState.ViewState.Content(
common = AddSendState.ViewState.Content.Common(
name = "",
maxAccessCount = null,
passwordInput = "",
noteInput = "",
isHideEmailChecked = false,
isDeactivateChecked = false,
deletionDate = ZonedDateTime
.now(clock)
// We want the default time to be midnight, so we remove all values
// beyond days
.truncatedTo(ChronoUnit.DAYS)
.plusWeeks(1),
expirationDate = null,
),
selectedType = AddSendState.ViewState.Content.SendType.Text(
input = "",
isHideByDefaultChecked = false,
),
)
is AddSendType.EditItem -> AddSendState.ViewState.Error(
"Not yet implemented".asText(),
)
},
dialogState = null,
isPremiumUser = authRepo.userStateFlow.value?.activeAccount?.isPremium == true,
baseWebSendUrl = environmentRepo.environment.environmentUrlData.baseWebSendUrl,
)
},
) {
init {
@ -320,12 +332,22 @@ class AddSendViewModel @Inject constructor(
*/
@Parcelize
data class AddSendState(
val addSendType: AddSendType,
val dialogState: DialogState?,
val viewState: ViewState,
val isPremiumUser: Boolean,
val baseWebSendUrl: String,
) : Parcelable {
/**
* Helper to determine the screen display name.
*/
val screenDisplayName: Text
get() = when (addSendType) {
AddSendType.AddItem -> R.string.add_send.asText()
is AddSendType.EditItem -> R.string.edit_send.asText()
}
/**
* Represents the specific view states for the [AddSendScreen].
*/

View file

@ -0,0 +1,23 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Represents the difference between create a completely new send and editing an existing one.
*/
sealed class AddSendType : Parcelable {
/**
* Indicates that we want to create a completely new send item.
*/
@Parcelize
data object AddItem : AddSendType()
/**
* Indicates that we want to edit an existing send item.
*
* @param sendItemId The ID of the send item to edit.
*/
@Parcelize
data class EditItem(val sendItemId: String) : AddSendType()
}

View file

@ -43,6 +43,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
onNavigateToVaultItem = {},
onNavigateToVaultEditItem = {},
onNavigateToAddSend = {},
onNavigateToEditSend = {},
onNavigateToDeleteAccount = {},
onNavigateToFolders = {},
onNavigateToPasswordHistory = {},

View file

@ -31,6 +31,7 @@ import io.mockk.runs
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
@ -38,6 +39,7 @@ import org.junit.Test
class SendScreenTest : BaseComposeTest() {
private var onNavigateToNewSendCalled = false
private var onNavigateToEditSendId: String? = null
private val intentHandler = mockk<IntentHandler> {
every { launchUri(any()) } just runs
@ -56,6 +58,7 @@ class SendScreenTest : BaseComposeTest() {
SendScreen(
viewModel = viewModel,
onNavigateToAddSend = { onNavigateToNewSendCalled = true },
onNavigateToEditSend = { onNavigateToEditSendId = it },
intentHandler = intentHandler,
)
}
@ -67,6 +70,13 @@ class SendScreenTest : BaseComposeTest() {
assertTrue(onNavigateToNewSendCalled)
}
@Test
fun `on NavigateToEditSend should call onNavigateToEditSend`() {
val sendId = "sendId1234"
mutableEventFlow.tryEmit(SendEvent.NavigateToEditSend(sendId))
assertEquals(sendId, onNavigateToEditSendId)
}
@Test
fun `on NavigateToAboutSend should call launchUri on intentHandler`() {
mutableEventFlow.tryEmit(SendEvent.NavigateToAboutSend)

View file

@ -142,12 +142,16 @@ class SendViewModelTest : BaseViewModelTest() {
}
@Test
fun `SendClick should emit ShowToast`() = runTest {
fun `SendClick should emit NavigateToEditSend`() = runTest {
val sendId = "sendId1234"
val sendItem = mockk<SendState.ViewState.Content.SendItem> {
every { id } returns sendId
}
val viewModel = createViewModel()
val sendItem = mockk<SendState.ViewState.Content.SendItem>()
viewModel.eventFlow.test {
viewModel.trySendAction(SendAction.SendClick(sendItem))
assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem())
assertEquals(SendEvent.NavigateToEditSend(sendId), awaitItem())
}
}

View file

@ -19,6 +19,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.IntentHandler
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
import com.x8bit.bitwarden.ui.util.isProgressBar
import io.mockk.every
import io.mockk.just
@ -596,6 +597,7 @@ class AddSendScreenTest : BaseComposeTest() {
)
private val DEFAULT_STATE = AddSendState(
addSendType = AddSendType.AddItem,
viewState = DEFAULT_VIEW_STATE,
dialogState = null,
isPremiumUser = false,

View file

@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendView
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
import io.mockk.coEvery
@ -394,8 +395,19 @@ class AddSendViewModelTest : BaseViewModelTest() {
private fun createViewModel(
state: AddSendState? = null,
addSendType: AddSendType = AddSendType.AddItem,
): AddSendViewModel = AddSendViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
savedStateHandle = SavedStateHandle().apply {
set("state", state)
set(
"add_send_item_type",
when (addSendType) {
AddSendType.AddItem -> "add"
is AddSendType.EditItem -> "edit"
},
)
set("edit_send_id", (addSendType as? AddSendType.EditItem)?.sendItemId)
},
authRepo = authRepository,
environmentRepo = environmentRepository,
clock = clock,
@ -432,6 +444,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
private const val DEFAULT_ENVIRONMENT_URL = "https://vault.bitwarden.com/#/send/"
private val DEFAULT_STATE = AddSendState(
addSendType = AddSendType.AddItem,
viewState = DEFAULT_VIEW_STATE,
dialogState = null,
isPremiumUser = false,