From cc685b230787a911ae8eb01fad50299b3e9b1b9c Mon Sep 17 00:00:00 2001
From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Date: Tue, 23 Sep 2025 17:41:58 -0400
Subject: [PATCH] [PM-26112] Handle Credential Exchange export requests
(#5928)
---
app/src/debug/AndroidManifest.xml | 12 +++++
.../com/x8bit/bitwarden/MainViewModel.kt | 13 +++++
.../manager/model/SpecialCircumstance.kt | 9 ++++
.../util/SpecialCircumstanceExtensions.kt | 10 ++++
.../platform/feature/rootnav/RootNavScreen.kt | 8 +++
.../feature/rootnav/RootNavViewModel.kt | 14 ++++-
.../exportitems/ExportItemsNavigation.kt | 43 +++++++++++++++
.../selectaccount/SelectAccountNavigation.kt | 42 +++++++++++++++
.../selectaccount/SelectAccountScreen.kt | 15 ++++++
.../com/x8bit/bitwarden/MainViewModelTest.kt | 39 ++++++++++++++
.../util/SpecialCircumstanceExtensionsTest.kt | 53 +++++++++++++++++++
.../feature/rootnav/RootNavScreenTest.kt | 12 +++++
.../feature/rootnav/RootNavViewModelTest.kt | 19 +++++++
13 files changed, 288 insertions(+), 1 deletion(-)
create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/ExportItemsNavigation.kt
create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/selectaccount/SelectAccountNavigation.kt
create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/selectaccount/SelectAccountScreen.kt
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
index 2d4e1a5eb2..f128653a30 100644
--- a/app/src/debug/AndroidManifest.xml
+++ b/app/src/debug/AndroidManifest.xml
@@ -20,6 +20,18 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/MainViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/MainViewModel.kt
index 6a59074f2c..e4d2551020 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/MainViewModel.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/MainViewModel.kt
@@ -5,6 +5,8 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.toast.ToastManager
+import com.bitwarden.cxf.model.ImportCredentialsRequestData
+import com.bitwarden.cxf.util.getProviderImportCredentialsRequest
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.manager.IntentManager
@@ -295,6 +297,7 @@ class MainViewModel @Inject constructor(
val getCredentialsRequest = intent.getGetCredentialsRequestOrNull()
val fido2AssertCredentialRequest = intent.getFido2AssertionRequestOrNull()
val providerGetPasswordRequest = intent.getProviderGetPasswordRequestOrNull()
+ val importCredentialsRequest = intent.getProviderImportCredentialsRequest()
when {
passwordlessRequestData != null -> {
authRepository.activeUserId?.let {
@@ -418,6 +421,16 @@ class MainViewModel @Inject constructor(
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AccountSecurityShortcut
}
+
+ importCredentialsRequest != null -> {
+ specialCircumstanceManager.specialCircumstance =
+ SpecialCircumstance.CredentialExchangeExport(
+ data = ImportCredentialsRequestData(
+ uri = importCredentialsRequest.uri,
+ requestJson = importCredentialsRequest.request.requestJson,
+ ),
+ )
+ }
}
}
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt
index 6435a7c417..d025990f72 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.manager.model
import android.os.Parcelable
import androidx.credentials.CredentialManager
+import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.bitwarden.ui.platform.manager.IntentManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
@@ -133,6 +134,14 @@ sealed class SpecialCircumstance : Parcelable {
@Parcelize
data object VerificationCodeShortcut : SpecialCircumstance()
+ /**
+ * The app was launched to select an account to export credentials from.
+ */
+ @Parcelize
+ data class CredentialExchangeExport(
+ val data: ImportCredentialsRequestData,
+ ) : SpecialCircumstance()
+
/**
* A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
* cleared after a successful login.
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt
index 919cebea5a..01451f3ac9 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt
@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.manager.util
+import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
@@ -71,3 +72,12 @@ fun SpecialCircumstance.toTotpDataOrNull(): TotpData? =
is SpecialCircumstance.AddTotpLoginItem -> this.data
else -> null
}
+
+/**
+ * Returns [ImportCredentialsRequestData] when contained in the given [SpecialCircumstance].
+ */
+fun SpecialCircumstance.toImportCredentialsRequestDataOrNull(): ImportCredentialsRequestData? =
+ when (this) {
+ is SpecialCircumstance.CredentialExchangeExport -> this.data
+ else -> null
+ }
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt
index 2fee64a0ca..54be454f6e 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt
@@ -62,6 +62,8 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addedit.navigateToAddEditSend
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.addedit.navigateToVaultAddEdit
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toVaultItemCipherType
+import com.x8bit.bitwarden.ui.vault.feature.exportitems.exportItemsGraph
+import com.x8bit.bitwarden.ui.vault.feature.exportitems.navigateToExportItemsGraph
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListingAsRoot
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
@@ -107,6 +109,7 @@ fun RootNavScreen(
setupUnlockDestinationAsRoot()
setupAutoFillDestinationAsRoot()
setupCompleteDestination()
+ exportItemsGraph()
}
val targetRoute = when (state) {
@@ -132,6 +135,7 @@ fun RootNavScreen(
is RootNavState.VaultUnlockedForFido2Assertion,
is RootNavState.VaultUnlockedForPasswordGet,
is RootNavState.VaultUnlockedForProviderGetCredentials,
+ is RootNavState.CredentialExchangeExport,
-> VaultUnlockedGraphRoute
RootNavState.OnboardingAccountLockSetup -> SetupUnlockRoute.AsRoot
@@ -270,6 +274,10 @@ fun RootNavScreen(
RootNavState.OnboardingStepsComplete -> {
navController.navigateToSetupCompleteScreen(rootNavOptions)
}
+
+ is RootNavState.CredentialExchangeExport -> {
+ navController.navigateToExportItemsGraph(rootNavOptions)
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt
index 56392cd67a..b741255088 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt
@@ -88,6 +88,10 @@ class RootNavViewModel @Inject constructor(
}
}
+ specialCircumstance is SpecialCircumstance.CredentialExchangeExport -> {
+ RootNavState.CredentialExchangeExport
+ }
+
userState.activeAccount.isVaultUnlocked &&
userState.shouldShowRemovePassword(authState = action.authState) -> {
RootNavState.RemovePassword
@@ -181,7 +185,9 @@ class RootNavViewModel @Inject constructor(
null,
-> RootNavState.VaultUnlocked(activeUserId = userState.activeAccount.userId)
- is SpecialCircumstance.RegistrationEvent -> {
+ is SpecialCircumstance.CredentialExchangeExport,
+ is SpecialCircumstance.RegistrationEvent,
+ -> {
throw IllegalStateException(
"Special circumstance should have been already handled.",
)
@@ -401,6 +407,12 @@ sealed class RootNavState : Parcelable {
*/
@Parcelize
data object OnboardingStepsComplete : RootNavState()
+
+ /**
+ * App should begin the export items flow.
+ */
+ @Parcelize
+ data object CredentialExchangeExport : RootNavState()
}
/**
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/ExportItemsNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/ExportItemsNavigation.kt
new file mode 100644
index 0000000000..1374975428
--- /dev/null
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/ExportItemsNavigation.kt
@@ -0,0 +1,43 @@
+@file:OmitFromCoverage
+
+package com.x8bit.bitwarden.ui.vault.feature.exportitems
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.navigation
+import com.bitwarden.annotation.OmitFromCoverage
+import com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount.SelectAccountRoute
+import com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount.selectAccountDestination
+import kotlinx.serialization.Serializable
+
+/**
+ * The type-safe route for the export items graph.
+ */
+@OmitFromCoverage
+@Serializable
+data object ExportItemsRoute
+
+/**
+ * Add export items destinations to the nav graph.
+ */
+fun NavGraphBuilder.exportItemsGraph() {
+ navigation(
+ startDestination = SelectAccountRoute,
+ ) {
+ selectAccountDestination(
+ onAccountSelected = {
+ // TODO: [PM-26110] Navigate to verify password screen.
+ },
+ )
+ }
+}
+
+/**
+ * Navigate to the export items graph.
+ */
+fun NavController.navigateToExportItemsGraph(
+ navOptions: NavOptions? = null,
+) {
+ navigate(route = ExportItemsRoute, navOptions = navOptions)
+}
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/selectaccount/SelectAccountNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/selectaccount/SelectAccountNavigation.kt
new file mode 100644
index 0000000000..06adfb1c3d
--- /dev/null
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/selectaccount/SelectAccountNavigation.kt
@@ -0,0 +1,42 @@
+@file:OmitFromCoverage
+
+package com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import com.bitwarden.annotation.OmitFromCoverage
+import com.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
+import kotlinx.serialization.Serializable
+
+/**
+ * The type-safe route for the select account screen.
+ */
+@OmitFromCoverage
+@Serializable
+data object SelectAccountRoute
+
+/**
+ * Add the [SelectAccountScreen] to the nav graph.
+ */
+fun NavGraphBuilder.selectAccountDestination(
+ onAccountSelected: (id: String) -> Unit,
+) {
+ composableWithRootPushTransitions {
+ SelectAccountScreen(
+ onAccountSelected = onAccountSelected,
+ )
+ }
+}
+
+/**
+ * Navigate to the [SelectAccountScreen].
+ */
+fun NavController.navigateToSelectAccountScreen(
+ navOptions: NavOptions? = null,
+) {
+ navigate(
+ route = SelectAccountRoute,
+ navOptions = navOptions,
+ )
+}
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/selectaccount/SelectAccountScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/selectaccount/SelectAccountScreen.kt
new file mode 100644
index 0000000000..9703706ec5
--- /dev/null
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/selectaccount/SelectAccountScreen.kt
@@ -0,0 +1,15 @@
+package com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+
+/**
+ * Top level screen for selecting an account to export items from.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SelectAccountScreen(
+ onAccountSelected: (id: String) -> Unit,
+) {
+ // TODO: [PM-26095] Implement select account screen.
+}
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/MainViewModelTest.kt
index dce91a3f0b..fd416151ab 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/MainViewModelTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/MainViewModelTest.kt
@@ -6,10 +6,14 @@ import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.provider.BiometricPromptResult
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.provider.ProviderGetCredentialRequest
+import androidx.credentials.providerevents.transfer.ImportCredentialsRequest
+import androidx.credentials.providerevents.transfer.ProviderImportCredentialsRequest
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
+import com.bitwarden.cxf.model.ImportCredentialsRequestData
+import com.bitwarden.cxf.util.getProviderImportCredentialsRequest
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.ui.platform.base.BaseViewModelTest
@@ -178,6 +182,7 @@ class MainViewModelTest : BaseViewModelTest() {
Intent::getCreateCredentialRequestOrNull,
Intent::getGetCredentialsRequestOrNull,
Intent::isAddTotpLoginItemFromAuthenticator,
+ Intent::getProviderImportCredentialsRequest,
)
mockkStatic(
Intent::isMyVaultShortcut,
@@ -209,6 +214,7 @@ class MainViewModelTest : BaseViewModelTest() {
Intent::getCreateCredentialRequestOrNull,
Intent::getGetCredentialsRequestOrNull,
Intent::isAddTotpLoginItemFromAuthenticator,
+ Intent::getProviderImportCredentialsRequest,
)
unmockkStatic(
Intent::isMyVaultShortcut,
@@ -1098,6 +1104,37 @@ class MainViewModelTest : BaseViewModelTest() {
verify { authRepository.switchAccount(userId) }
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `on ReceiveNewIntent with import credentials request data should set the special circumstance to CredentialExchangeExport`() {
+ val viewModel = createViewModel()
+ val importCredentialsRequestData = ProviderImportCredentialsRequest(
+ request = ImportCredentialsRequest("mockRequestJson"),
+ callingAppInfo = mockk(),
+ uri = mockk(),
+ credId = "mockCredId",
+ )
+ val mockIntent = createMockIntent(
+ mockProviderImportCredentialsRequest = importCredentialsRequestData,
+ )
+
+ viewModel.trySendAction(
+ MainAction.ReceiveNewIntent(
+ intent = mockIntent,
+ ),
+ )
+
+ assertEquals(
+ SpecialCircumstance.CredentialExchangeExport(
+ data = ImportCredentialsRequestData(
+ uri = importCredentialsRequestData.uri,
+ requestJson = importCredentialsRequestData.request.requestJson,
+ ),
+ ),
+ specialCircumstanceManager.specialCircumstance,
+ )
+ }
+
@Suppress("MaxLineLength")
@Test
fun `on ResumeScreenDataReceived with null value, should call AppResumeManager clearResumeScreen`() {
@@ -1209,6 +1246,7 @@ private fun createMockIntent(
mockIsPasswordGeneratorShortcut: Boolean = false,
mockIsAccountSecurityShortcut: Boolean = false,
mockIsAddTotpLoginItemFromAuthenticator: Boolean = false,
+ mockProviderImportCredentialsRequest: ProviderImportCredentialsRequest? = null,
): Intent = mockk {
every { getTotpDataOrNull() } returns mockTotpData
every { getPasswordlessRequestDataIntentOrNull() } returns mockPasswordlessRequestData
@@ -1223,6 +1261,7 @@ private fun createMockIntent(
every { isPasswordGeneratorShortcut } returns mockIsPasswordGeneratorShortcut
every { isAccountSecurityShortcut } returns mockIsAccountSecurityShortcut
every { isAddTotpLoginItemFromAuthenticator() } returns mockIsAddTotpLoginItemFromAuthenticator
+ every { getProviderImportCredentialsRequest() } returns mockProviderImportCredentialsRequest
}
private val FIXED_CLOCK: Clock = Clock.fixed(
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt
index 6d53bc9b5b..da7247de8b 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt
@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.manager.util
import androidx.core.os.bundleOf
+import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
@@ -347,4 +348,56 @@ class SpecialCircumstanceExtensionsTest {
assertNull(specialCircumstance.toTotpDataOrNull())
}
}
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `toImportCredentialsRequestDataOrNull should return a non-null value for ImportCredentials`() {
+ val importCredentialsRequest = ImportCredentialsRequestData(
+ uri = mockk(),
+ requestJson = "",
+ )
+ assertEquals(
+ importCredentialsRequest,
+ SpecialCircumstance
+ .CredentialExchangeExport(
+ data = importCredentialsRequest,
+ )
+ .toImportCredentialsRequestDataOrNull(),
+ )
+ }
+
+ @Test
+ fun `toImportCredentialsRequestDataOrNull should return a null value for other types`() {
+ listOf(
+ SpecialCircumstance.AutofillSelection(
+ autofillSelectionData = mockk(),
+ shouldFinishWhenComplete = true,
+ ),
+ SpecialCircumstance.AutofillSave(
+ autofillSaveItem = mockk(),
+ ),
+ SpecialCircumstance.ShareNewSend(
+ data = mockk(),
+ shouldFinishWhenComplete = true,
+ ),
+ mockk(),
+ SpecialCircumstance.PasswordlessRequest(
+ passwordlessRequestData = mockk(),
+ shouldFinishWhenComplete = true,
+ ),
+ SpecialCircumstance.ProviderCreateCredential(
+ createCredentialRequest = mockk(),
+ ),
+ SpecialCircumstance.ProviderGetCredentials(
+ getCredentialsRequest = mockk(),
+ ),
+ SpecialCircumstance.Fido2Assertion(
+ fido2AssertionRequest = mockk(),
+ ),
+ SpecialCircumstance.GeneratorShortcut,
+ SpecialCircumstance.VaultShortcut,
+ ).forEach { specialCircumstance ->
+ assertNull(specialCircumstance.toImportCredentialsRequestDataOrNull())
+ }
+ }
}
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt
index 6c1e7480da..2560a73509 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt
@@ -23,6 +23,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addedit.ModeType
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditMode
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditRoute
+import com.x8bit.bitwarden.ui.vault.feature.exportitems.ExportItemsRoute
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.ItemListingType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingRoute
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
@@ -426,6 +427,17 @@ class RootNavScreenTest : BitwardenComposeTest() {
)
}
}
+
+ // Make sure navigating to export items graph works as expected:
+ rootNavStateFlow.value = RootNavState.CredentialExchangeExport
+ composeTestRule.runOnIdle {
+ verify {
+ mockNavHostController.navigate(
+ route = ExportItemsRoute,
+ navOptions = expectedNavOptions,
+ )
+ }
+ }
}
}
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt
index 012927d9df..bde6d03529 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt
@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.platform.feature.rootnav
import androidx.core.os.bundleOf
+import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.network.model.JwtTokenDataJson
@@ -1392,6 +1393,24 @@ class RootNavViewModelTest : BaseViewModelTest() {
)
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `when SpecialCircumstance is CredentialExchangeExport the nav state should be CredentialExchangeExport`() {
+ specialCircumstanceManager.specialCircumstance =
+ SpecialCircumstance.CredentialExchangeExport(
+ data = ImportCredentialsRequestData(
+ uri = mockk(),
+ requestJson = "mockRequestJson",
+ ),
+ )
+ mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
+ val viewModel = createViewModel()
+ assertEquals(
+ RootNavState.CredentialExchangeExport,
+ viewModel.stateFlow.value,
+ )
+ }
+
private fun createViewModel(): RootNavViewModel =
RootNavViewModel(
authRepository = authRepository,