diff --git a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt index 32055a1d2..60aa81c52 100644 --- a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt @@ -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( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt index 28247d8cf..113a75c74 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt @@ -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. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt index 01b2bc96d..65e3db91b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt @@ -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 } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index da5ca4af1..67b99dbf9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -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( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index adb7b5845..7c6451ccf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -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. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index 50eba0adb..ab4dcc23f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -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() val shareData = mockk() + 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() val autofillSelectionData = mockk() + 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() + val autofillSaveItem = mockk() + 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() val shareData = mockk() + 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() val autofillSelectionData = mockk() + 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() + val autofillSaveItem = mockk() + 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 `() = diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt index 265885382..18d233a6f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt @@ -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()) + } } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt index be2f6cbce..2c169f19f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index 10cae5f68..c60e38d6f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -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`() {