Navigate to the Vault Listing screen from autofill (#810)

This commit is contained in:
Brian Yencho 2024-01-27 13:48:39 -06:00 committed by Álison Fernandes
parent 0411ccd3f9
commit f2a7998bb0
16 changed files with 374 additions and 27 deletions

View file

@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -91,8 +92,19 @@ class MainViewModel @Inject constructor(
intent: Intent,
isFirstIntent: Boolean,
) {
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
val shareData = intentManager.getShareDataFromIntent(intent)
when {
autofillSelectionData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AutofillSelection(
autofillSelectionData = autofillSelectionData,
// Allow users back into the already-running app when completing the
// autofill task when this is not the first intent.
shouldFinishWhenComplete = isFirstIntent,
)
}
shareData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ShareNewSend(

View file

@ -0,0 +1,25 @@
package com.x8bit.bitwarden.data.autofill.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Represents data for a manual autofill selection.
*
* @property type The type of autofill selection that must be made.
* @property uri A URI representing the location where data should be filled (if available).
*/
@Parcelize
class AutofillSelectionData(
val type: Type,
val uri: String?,
) : Parcelable {
/**
* The type of selection the user must make.
*/
enum class Type {
CARD,
LOGIN,
}
}

View file

@ -0,0 +1,46 @@
@file:OmitFromCoverage
package com.x8bit.bitwarden.data.autofill.util
import android.content.Context
import android.content.Intent
import android.os.Build
import com.x8bit.bitwarden.MainActivity
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
private const val AUTOFILL_SELECTION_DATA_KEY = "autofill-selection-data"
/**
* Creates an [Intent] in order to send the user to a manual selection process for autofill.
*/
fun createAutofillSelectionIntent(
context: Context,
type: AutofillSelectionData.Type,
uri: String?,
): Intent =
Intent(
context,
MainActivity::class.java,
)
.apply {
putExtra(
AUTOFILL_SELECTION_DATA_KEY,
AutofillSelectionData(
type = type,
uri = uri,
),
)
}
/**
* Checks if the given [Intent] contains data about an ongoing manual autofill selection process.
* The [AutofillSelectionData] will be returned when present.
*/
fun Intent.getAutofillSelectionDataOrNull(): AutofillSelectionData? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
this.getParcelableExtra(AUTOFILL_SELECTION_DATA_KEY, AutofillSelectionData::class.java)
} else {
this.getParcelableExtra(AUTOFILL_SELECTION_DATA_KEY)
}
}

View file

@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.autofill.util
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.Presentations
@ -11,8 +10,9 @@ import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi
import com.x8bit.bitwarden.MainActivity
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.model.FilledData
import com.x8bit.bitwarden.data.autofill.model.FilledItem
import com.x8bit.bitwarden.ui.autofill.buildVaultItemAutofillRemoteViews
@ -31,11 +31,14 @@ val FilledData.fillableAutofillIds: List<AutofillId>
fun FilledData.buildVaultItemDataset(
autofillAppInfo: AutofillAppInfo,
): Dataset {
val intent = Intent(
autofillAppInfo.context,
MainActivity::class.java,
val intent = createAutofillSelectionIntent(
context = autofillAppInfo.context,
type = when (this.originalPartition) {
is AutofillPartition.Card -> AutofillSelectionData.Type.CARD
is AutofillPartition.Login -> AutofillSelectionData.Type.LOGIN
},
uri = this.uri,
)
// TODO: Add additional data to the Intent to be pulled out in the app (BIT-1296)
val pendingIntent = PendingIntent
.getActivity(

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.manager.model
import android.os.Parcelable
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import kotlinx.parcelize.Parcelize
@ -17,4 +18,13 @@ sealed class SpecialCircumstance : Parcelable {
val data: IntentManager.ShareData,
val shouldFinishWhenComplete: Boolean,
) : SpecialCircumstance()
/**
* The app was launched in order to allow the user to manually select data for autofill.
*/
@Parcelize
data class AutofillSelection(
val autofillSelectionData: AutofillSelectionData,
val shouldFinishWhenComplete: Boolean,
) : SpecialCircumstance()
}

View file

@ -17,6 +17,7 @@ import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination
import com.x8bit.bitwarden.ui.platform.feature.rootnav.util.toVaultItemListingType
import com.x8bit.bitwarden.ui.platform.feature.splash.SPLASH_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.splash.navigateToSplash
import com.x8bit.bitwarden.ui.platform.feature.splash.splashDestination
@ -26,6 +27,7 @@ import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraph
import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders
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.itemlisting.navigateToVaultItemListingAsRoot
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.util.concurrent.atomic.AtomicReference
@ -76,7 +78,8 @@ fun RootNavScreen(
RootNavState.Splash -> SPLASH_ROUTE
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
is RootNavState.VaultUnlocked,
RootNavState.VaultUnlockedForNewSend,
is RootNavState.VaultUnlockedForAutofillSelection,
is RootNavState.VaultUnlockedForNewSend,
-> VAULT_UNLOCKED_GRAPH_ROUTE
}
val currentRoute = navController.currentDestination?.rootLevelRoute()
@ -102,7 +105,7 @@ fun RootNavScreen(
restoreState = false
}
when (state) {
when (val currentState = state) {
RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions)
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
@ -114,6 +117,14 @@ fun RootNavScreen(
navOptions = rootNavOptions,
)
}
is RootNavState.VaultUnlockedForAutofillSelection -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToVaultItemListingAsRoot(
vaultItemListingType = currentState.type.toVaultItemListingType(),
navOptions = rootNavOptions,
)
}
}
}

View file

@ -4,6 +4,7 @@ import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
@ -64,10 +65,15 @@ class RootNavViewModel @Inject constructor(
userState.activeAccount.isVaultUnlocked -> {
when (specialCircumstance) {
is SpecialCircumstance.AutofillSelection -> {
RootNavState.VaultUnlockedForAutofillSelection(
type = specialCircumstance.autofillSelectionData.type,
)
}
is SpecialCircumstance.ShareNewSend -> RootNavState.VaultUnlockedForNewSend
null,
-> {
null -> {
RootNavState.VaultUnlocked(
activeUserId = userState.activeAccount.userId,
)
@ -111,6 +117,14 @@ sealed class RootNavState : Parcelable {
val activeUserId: String,
) : RootNavState()
/**
* App should show a selection screen for autofill for an unlocked user.
*/
@Parcelize
data class VaultUnlockedForAutofillSelection(
val type: AutofillSelectionData.Type,
) : RootNavState()
/**
* App should show the new send screen for an unlocked user.
*/

View file

@ -0,0 +1,13 @@
package com.x8bit.bitwarden.ui.platform.feature.rootnav.util
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
/**
* Derives a [VaultItemListingType] from the given [AutofillSelectionData.Type].
*/
fun AutofillSelectionData.Type.toVaultItemListingType(): VaultItemListingType =
when (this) {
AutofillSelectionData.Type.CARD -> VaultItemListingType.Card
AutofillSelectionData.Type.LOGIN -> VaultItemListingType.Login
}

View file

@ -34,6 +34,7 @@ import com.x8bit.bitwarden.ui.vault.feature.attachments.attachmentDestination
import com.x8bit.bitwarden.ui.vault.feature.attachments.navigateToAttachment
import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem
import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestinationAsRoot
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.navigateToManualCodeEntryScreen
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.vaultManualCodeEntryDestination
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.navigateToVaultMoveToOrganization
@ -62,6 +63,17 @@ fun NavGraphBuilder.vaultUnlockedGraph(
startDestination = VAULT_UNLOCKED_NAV_BAR_ROUTE,
route = VAULT_UNLOCKED_GRAPH_ROUTE,
) {
vaultItemListingDestinationAsRoot(
onNavigateBack = { navController.popBackStack() },
onNavigateToVaultItemScreen = { navController.navigateToVaultItem(vaultItemId = it) },
onNavigateToVaultAddItemScreen = {
navController.navigateToVaultAddEdit(VaultAddEditType.AddItem)
},
onNavigateToSearchVault = { navController.navigateToSearch(searchType = it) },
onNavigateToVaultEditItemScreen = {
navController.navigateToVaultAddEdit(VaultAddEditType.EditItem(it))
},
)
vaultUnlockedNavBarDestination(
onNavigateToExportVault = { navController.navigateToExportVault() },
onNavigateToFolders = { navController.navigateToFolders() },

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.composableWithPushTransitions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithStayTransitions
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
@ -21,11 +22,15 @@ private const val SEND_FILE: String = "send_file"
private const val SEND_TEXT: String = "send_text"
private const val TRASH: String = "trash"
private const val VAULT_ITEM_LISTING_PREFIX: String = "vault_item_listing"
private const val VAULT_ITEM_LISTING_AS_ROOT_PREFIX: String = "vault_item_listing_as_root"
private const val VAULT_ITEM_LISTING_TYPE: String = "vault_item_listing_type"
private const val ID: String = "id"
private const val VAULT_ITEM_LISTING_ROUTE: String =
"$VAULT_ITEM_LISTING_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" +
"?$ID={$ID}"
private const val VAULT_ITEM_LISTING_AS_ROOT_ROUTE: String =
"$VAULT_ITEM_LISTING_AS_ROOT_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" +
"?$ID={$ID}"
private const val SEND_ITEM_LISTING_PREFIX: String = "send_item_listing"
private const val SEND_ITEM_LISTING_ROUTE: String =
"$SEND_ITEM_LISTING_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" +
@ -70,6 +75,39 @@ fun NavGraphBuilder.vaultItemListingDestination(
)
}
/**
* Add the [VaultItemListingScreen] to the nav graph.
*/
fun NavGraphBuilder.vaultItemListingDestinationAsRoot(
onNavigateBack: () -> Unit,
onNavigateToVaultItemScreen: (id: String) -> Unit,
onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit,
onNavigateToVaultAddItemScreen: () -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
) {
composableWithStayTransitions(
route = VAULT_ITEM_LISTING_AS_ROOT_ROUTE,
arguments = listOf(
navArgument(
name = VAULT_ITEM_LISTING_TYPE,
builder = {
type = NavType.StringType
},
),
),
) {
VaultItemListingScreen(
onNavigateBack = onNavigateBack,
onNavigateToVaultItem = onNavigateToVaultItemScreen,
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
onNavigateToSearch = { onNavigateToSearchVault(it as SearchType.Vault) },
onNavigateToAddSendItem = {},
onNavigateToEditSendItem = {},
)
}
}
/**
* Add the [VaultItemListingScreen] to the nav graph.
*/
@ -110,7 +148,9 @@ private fun NavGraphBuilder.internalVaultItemListingDestination(
arguments = listOf(
navArgument(
name = VAULT_ITEM_LISTING_TYPE,
builder = { type = NavType.StringType },
builder = {
type = NavType.StringType
},
),
navArgument(
name = ID,
@ -147,6 +187,20 @@ fun NavController.navigateToVaultItemListing(
)
}
/**
* Navigate to the [VaultItemListingScreen] for vault.
*/
fun NavController.navigateToVaultItemListingAsRoot(
vaultItemListingType: VaultItemListingType,
navOptions: NavOptions? = null,
) {
navigate(
route = "$VAULT_ITEM_LISTING_AS_ROOT_PREFIX/${vaultItemListingType.toTypeString()}" +
"?$ID=${vaultItemListingType.toIdOrNull()}",
navOptions = navOptions,
)
}
/**
* Navigate to the [VaultItemListingScreen] for sends.
*/

View file

@ -4,7 +4,10 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
@ -44,7 +47,7 @@ import javax.inject.Inject
* and launches [VaultItemListingEvent] for the [VaultItemListingScreen].
*/
@HiltViewModel
@Suppress("MagicNumber", "TooManyFunctions")
@Suppress("MagicNumber", "TooManyFunctions", "LongParameterList")
class VaultItemListingViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val clock: Clock,
@ -52,19 +55,26 @@ class VaultItemListingViewModel @Inject constructor(
private val vaultRepository: VaultRepository,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
private val specialCircumstanceManager: SpecialCircumstanceManager,
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
initialState = VaultItemListingState(
itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle)
.vaultItemListingType
.toItemListingType(),
viewState = VaultItemListingState.ViewState.Loading,
vaultFilterType = vaultRepository.vaultFilterType,
baseWebSendUrl = environmentRepository.environment.environmentUrlData.baseWebSendUrl,
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
dialogState = null,
),
initialState = run {
val specialCircumstance =
specialCircumstanceManager.specialCircumstance as? SpecialCircumstance.AutofillSelection
VaultItemListingState(
itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle)
.vaultItemListingType
.toItemListingType(),
viewState = VaultItemListingState.ViewState.Loading,
vaultFilterType = vaultRepository.vaultFilterType,
baseWebSendUrl = environmentRepository.environment.environmentUrlData.baseWebSendUrl,
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
dialogState = null,
autofillSelectionData = specialCircumstance?.autofillSelectionData,
shouldFinishOnComplete = specialCircumstance?.shouldFinishWhenComplete ?: false,
)
},
) {
init {
@ -521,7 +531,10 @@ data class VaultItemListingState(
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
val dialogState: DialogState?,
// Internal
private val isPullToRefreshSettingEnabled: Boolean,
private val autofillSelectionData: AutofillSelectionData? = null,
private val shouldFinishOnComplete: Boolean = false,
) {
/**
@ -530,6 +543,12 @@ data class VaultItemListingState(
val isPullToRefreshEnabled: Boolean
get() = isPullToRefreshSettingEnabled && viewState.isPullToRefreshEnabled
/**
* Whether or not this represents a listing screen for autofill.
*/
val isAutofill: Boolean
get() = autofillSelectionData != null
/**
* Represents the current state of any dialogs on the screen.
*/

View file

@ -5,7 +5,8 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -107,7 +108,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val shareData = mockk<IntentManager.ShareData>()
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData
viewModel.trySendAction(
@ -124,13 +125,36 @@ class MainViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveFirstIntent with autofill data should set the special circumstance to AutofillSelection`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val autofillSelectionData = mockk<AutofillSelectionData>()
every { mockIntent.getAutofillSelectionDataOrNull() } returns autofillSelectionData
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
viewModel.trySendAction(
MainAction.ReceiveFirstIntent(
intent = mockIntent,
),
)
assertEquals(
SpecialCircumstance.AutofillSelection(
autofillSelectionData = autofillSelectionData,
shouldFinishWhenComplete = true,
),
specialCircumstanceManager.specialCircumstance,
)
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveNewIntent with share data should set the special circumstance to ShareNewSend`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val shareData = mockk<IntentManager.ShareData>()
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData
viewModel.trySendAction(
@ -147,6 +171,29 @@ class MainViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveNewIntent with autofill data should set the special circumstance to AutofillSelection`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val autofillSelectionData = mockk<AutofillSelectionData>()
every { mockIntent.getAutofillSelectionDataOrNull() } returns autofillSelectionData
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
viewModel.trySendAction(
MainAction.ReceiveNewIntent(
intent = mockIntent,
),
)
assertEquals(
SpecialCircumstance.AutofillSelection(
autofillSelectionData = autofillSelectionData,
shouldFinishWhenComplete = false,
),
specialCircumstanceManager.specialCircumstance,
)
}
@Suppress("MaxLineLength")
@Test
fun `changes in the allowed screen capture value should result in emissions of ScreenCaptureSettingChange `() =

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.platform.feature.rootnav
import androidx.navigation.navOptions
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.FakeNavHostController
import io.mockk.every
@ -98,5 +99,17 @@ class RootNavScreenTest : BaseComposeTest() {
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked for autofill works as expected:
rootNavStateFlow.value =
RootNavState.VaultUnlockedForAutofillSelection(
type = AutofillSelectionData.Type.LOGIN,
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_item_listing_as_root/login",
navOptions = expectedNavOptions,
)
}
}
}

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.model.Environment
@ -145,6 +146,46 @@ class RootNavViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault but there is an AutofillSelection special circumstance the nav state should be VaultUnlockedForAutofillSelection`() {
val autofillSelectionData = AutofillSelectionData(
type = AutofillSelectionData.Type.LOGIN,
uri = "uri",
)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AutofillSelection(
autofillSelectionData = autofillSelectionData,
shouldFinishWhenComplete = true,
)
mutableUserStateFlow.tryEmit(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
isBiometricsEnabled = false,
organizations = emptyList(),
),
),
),
)
val viewModel = createViewModel()
assertEquals(
RootNavState.VaultUnlockedForAutofillSelection(
type = AutofillSelectionData.Type.LOGIN,
),
viewModel.stateFlow.value,
)
}
@Test
fun `when the active user has a locked vault the nav state should be VaultLocked`() {
mutableUserStateFlow.tryEmit(

View file

@ -0,0 +1,22 @@
package com.x8bit.bitwarden.ui.platform.feature.rootnav.util
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class AutofillSelectionDataExtensionsTest {
@Test
fun `toVaultItemListingType should return the correct result for each type`() {
mapOf(
AutofillSelectionData.Type.CARD to VaultItemListingType.Card,
AutofillSelectionData.Type.LOGIN to VaultItemListingType.Login,
)
.forEach { (type, expected) ->
assertEquals(
expected,
type.toVaultItemListingType(),
)
}
}
}

View file

@ -4,6 +4,7 @@ import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -76,6 +77,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow
every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshEnabledFlow
}
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
private val initialState = createVaultItemListingState()
private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType(
vaultItemListingType = VaultItemListingType.Login,
@ -941,6 +943,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
vaultRepository = vaultRepository,
environmentRepository = environmentRepository,
settingsRepository = settingsRepository,
specialCircumstanceManager = specialCircumstanceManager,
)
@Suppress("MaxLineLength")
@ -957,5 +960,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
isPullToRefreshSettingEnabled = false,
dialogState = null,
autofillSelectionData = null,
shouldFinishOnComplete = false,
)
}