Allow users to navigate to Add Item screen for autofill save (#900)

This commit is contained in:
Brian Yencho 2024-01-31 13:27:54 -06:00 committed by Álison Fernandes
parent d711360bad
commit 10471a7ea6
9 changed files with 161 additions and 8 deletions

View file

@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
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
@ -110,9 +111,17 @@ class MainViewModel @Inject constructor(
intent: Intent,
isFirstIntent: Boolean,
) {
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
val shareData = intentManager.getShareDataFromIntent(intent)
when {
autofillSaveItem != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AutofillSave(
autofillSaveItem = autofillSaveItem,
)
}
autofillSelectionData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AutofillSelection(

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.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import kotlinx.parcelize.Parcelize
@ -19,6 +20,15 @@ sealed class SpecialCircumstance : Parcelable {
val shouldFinishWhenComplete: Boolean,
) : SpecialCircumstance()
/**
* The app was launched via the autofill framework in order to allow the user to manually save
* data that was entered in an external form.
*/
@Parcelize
data class AutofillSave(
val autofillSaveItem: AutofillSaveItem,
) : SpecialCircumstance()
/**
* The app was launched in order to allow the user to manually select data for autofill.
*/

View file

@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
*/
fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData? =
when (this) {
is SpecialCircumstance.AutofillSave -> null
is SpecialCircumstance.AutofillSelection -> this.autofillSelectionData
is SpecialCircumstance.ShareNewSend -> null
}

View file

@ -32,7 +32,9 @@ import com.x8bit.bitwarden.ui.platform.theme.NonNullExitTransitionProvider
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.addedit.navigateToVaultAddEdit
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListingAsRoot
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.util.concurrent.atomic.AtomicReference
@ -85,6 +87,7 @@ fun RootNavScreen(
RootNavState.Splash -> SPLASH_ROUTE
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
is RootNavState.VaultUnlocked,
is RootNavState.VaultUnlockedForAutofillSave,
is RootNavState.VaultUnlockedForAutofillSelection,
is RootNavState.VaultUnlockedForNewSend,
-> VAULT_UNLOCKED_GRAPH_ROUTE
@ -126,6 +129,14 @@ fun RootNavScreen(
)
}
is RootNavState.VaultUnlockedForAutofillSave -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToVaultAddEdit(
vaultAddEditType = VaultAddEditType.AddItem,
navOptions = rootNavOptions,
)
}
is RootNavState.VaultUnlockedForAutofillSelection -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToVaultItemListingAsRoot(

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.AutofillSaveItem
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
@ -67,6 +68,12 @@ class RootNavViewModel @Inject constructor(
userState.activeAccount.isVaultUnlocked -> {
when (specialCircumstance) {
is SpecialCircumstance.AutofillSave -> {
RootNavState.VaultUnlockedForAutofillSave(
autofillSaveItem = specialCircumstance.autofillSaveItem,
)
}
is SpecialCircumstance.AutofillSelection -> {
RootNavState.VaultUnlockedForAutofillSelection(
activeUserId = userState.activeAccount.userId,
@ -126,6 +133,15 @@ sealed class RootNavState : Parcelable {
val activeUserId: String,
) : RootNavState()
/**
* App should show an add item screen for a user to complete the saving of data collected by
* the autofill framework.
*/
@Parcelize
data class VaultUnlockedForAutofillSave(
val autofillSaveItem: AutofillSaveItem,
) : RootNavState()
/**
* App should show a selection screen for autofill for an unlocked user.
*/

View file

@ -8,7 +8,9 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
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
@ -129,6 +131,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val shareData = mockk<IntentManager.ShareData>()
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData
@ -152,6 +155,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val autofillSelectionData = mockk<AutofillSelectionData>()
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns autofillSelectionData
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
@ -169,12 +173,36 @@ class MainViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveFirstIntent with an autofill save item should set the special circumstance to AutofillSave`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val autofillSaveItem = mockk<AutofillSaveItem>()
every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
viewModel.trySendAction(
MainAction.ReceiveFirstIntent(
intent = mockIntent,
),
)
assertEquals(
SpecialCircumstance.AutofillSave(
autofillSaveItem = autofillSaveItem,
),
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.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData
@ -198,6 +226,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val autofillSelectionData = mockk<AutofillSelectionData>()
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns autofillSelectionData
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
@ -215,6 +244,29 @@ class MainViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveNewIntent with an autofill save item should set the special circumstance to AutofillSave`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val autofillSaveItem = mockk<AutofillSaveItem>()
every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
viewModel.trySendAction(
MainAction.ReceiveNewIntent(
intent = mockIntent,
),
)
assertEquals(
SpecialCircumstance.AutofillSave(
autofillSaveItem = autofillSaveItem,
),
specialCircumstanceManager.specialCircumstance,
)
}
@Suppress("MaxLineLength")
@Test
fun `changes in the allowed screen capture value should result in emissions of ScreenCaptureSettingChange `() =

View file

@ -27,14 +27,18 @@ class SpecialCircumstanceExtensionsTest {
}
@Test
fun `toAutofillSelectionDataOrNull should a non-null value for other types`() {
assertNull(
SpecialCircumstance
.ShareNewSend(
data = mockk(),
shouldFinishWhenComplete = true,
)
.toAutofillSelectionDataOrNull(),
fun `toAutofillSelectionDataOrNull should a null value for other types`() {
listOf(
SpecialCircumstance.AutofillSave(
autofillSaveItem = mockk(),
),
SpecialCircumstance.ShareNewSend(
data = mockk(),
shouldFinishWhenComplete = true,
),
)
.forEach { specialCircumstance ->
assertNull(specialCircumstance.toAutofillSelectionDataOrNull())
}
}
}

View file

@ -109,6 +109,18 @@ class RootNavScreenTest : BaseComposeTest() {
)
}
// Make sure navigating to vault unlocked for autofill save works as expected:
rootNavStateFlow.value =
RootNavState.VaultUnlockedForAutofillSave(
autofillSaveItem = mockk(),
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_add_edit_item/add",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked for autofill works as expected:
rootNavStateFlow.value =
RootNavState.VaultUnlockedForAutofillSelection(

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.AutofillSaveItem
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
@ -175,6 +176,43 @@ class RootNavViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault but there is an AutofillSave special circumstance the nav state should be VaultUnlockedForAutofillSave`() {
val autofillSaveItem: AutofillSaveItem = mockk()
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AutofillSave(
autofillSaveItem = autofillSaveItem,
)
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,
needsPasswordReset = false,
isBiometricsEnabled = false,
organizations = emptyList(),
),
),
),
)
val viewModel = createViewModel()
assertEquals(
RootNavState.VaultUnlockedForAutofillSave(
autofillSaveItem = autofillSaveItem,
),
viewModel.stateFlow.value,
)
}
@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`() {