From 1446e43c464bfad818cf5ecc81eca4dd917970d7 Mon Sep 17 00:00:00 2001
From: Andrew Haisting <142518658+ahaisting-livefront@users.noreply.github.com>
Date: Wed, 16 Oct 2024 09:45:10 -0500
Subject: [PATCH] BITAU-105 Add support for deep link to account security
(#4063)
---
app/src/main/AndroidManifest.xml | 5 ++
.../java/com/x8bit/bitwarden/MainViewModel.kt | 7 +++
.../manager/model/SpecialCircumstance.kt | 6 +++
.../feature/rootnav/RootNavViewModel.kt | 1 +
.../feature/settings/SettingsScreen.kt | 1 +
.../feature/settings/SettingsViewModel.kt | 18 +++++++
.../VaultUnlockedNavBarScreen.kt | 6 ++-
.../VaultUnlockedNavBarViewModel.kt | 11 ++++
.../ui/platform/util/ShortcutUtils.kt | 6 +++
.../com/x8bit/bitwarden/MainViewModelTest.kt | 53 +++++++++++++++++++
.../feature/rootnav/RootNavViewModelTest.kt | 38 +++++++++++++
.../feature/settings/SettingsScreenTest.kt | 19 +++++++
.../feature/settings/SettingsViewModelTest.kt | 45 +++++++++++++---
.../VaultUnlockedNavBarScreenTest.kt | 15 ++++++
.../VaultUnlockedNavBarViewModelTest.kt | 21 ++++++++
15 files changed, 243 insertions(+), 9 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e36381a54..96675e7a1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -98,6 +98,11 @@
+
+
+
+
+
{
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.VaultShortcut
}
+
+ hasAccountSecurityShortcut -> {
+ specialCircumstanceManager.specialCircumstance =
+ SpecialCircumstance.AccountSecurityShortcut
+ }
}
}
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 2bcd57fe1..969115a43 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
@@ -98,6 +98,12 @@ sealed class SpecialCircumstance : Parcelable {
@Parcelize
data object VaultShortcut : SpecialCircumstance()
+ /**
+ * The app was launched via deeplink to the account security screen.
+ */
+ @Parcelize
+ data object AccountSecurityShortcut : 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/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 ca13a34a6..74716bcaf 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
@@ -151,6 +151,7 @@ class RootNavViewModel @Inject constructor(
)
}
+ SpecialCircumstance.AccountSecurityShortcut,
SpecialCircumstance.GeneratorShortcut,
SpecialCircumstance.VaultShortcut,
null,
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt
index 3991c3d94..e3238ff55 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt
@@ -67,6 +67,7 @@ fun SettingsScreen(
SettingsEvent.NavigateAutoFill -> onNavigateToAutoFill()
SettingsEvent.NavigateOther -> onNavigateToOther()
SettingsEvent.NavigateVault -> onNavigateToVault()
+ SettingsEvent.NavigateAccountSecurityShortcut -> onNavigateToAccountSecurity()
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt
index 394f808ad..c3af8d010 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt
@@ -3,8 +3,11 @@ package com.x8bit.bitwarden.ui.platform.feature.settings
import androidx.compose.material3.Text
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
+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
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
+import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -20,6 +23,7 @@ import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
settingsRepository: SettingsRepository,
+ specialCircumstanceManager: SpecialCircumstanceManager,
) : BaseViewModel(
initialState = SettingsState(
securityCount = settingsRepository.allSecuritySettingsBadgeCountFlow.value,
@@ -39,6 +43,15 @@ class SettingsViewModel @Inject constructor(
}
.onEach(::sendAction)
.launchIn(viewModelScope)
+
+ when (specialCircumstanceManager.specialCircumstance) {
+ SpecialCircumstance.AccountSecurityShortcut -> {
+ sendEvent(SettingsEvent.NavigateAccountSecurityShortcut)
+ specialCircumstanceManager.specialCircumstance = null
+ }
+
+ else -> Unit
+ }
}
override fun handleAction(action: SettingsAction): Unit = when (action) {
@@ -115,6 +128,11 @@ sealed class SettingsEvent {
*/
data object NavigateAccountSecurity : SettingsEvent()
+ /**
+ * Navigate to the account security screen.
+ */
+ data object NavigateAccountSecurityShortcut : SettingsEvent(), BackgroundEvent
+
/**
* Navigate to the appearance screen.
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt
index 7b01a79d8..a447e99d6 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt
@@ -58,7 +58,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
/**
* Top level composable for the Vault Unlocked Screen.
*/
-@Suppress("LongParameterList")
+@Suppress("LongParameterList", "LongMethod")
@Composable
fun VaultUnlockedNavBarScreen(
viewModel: VaultUnlockedNavBarViewModel = hiltViewModel(),
@@ -104,6 +104,10 @@ fun VaultUnlockedNavBarScreen(
VaultUnlockedNavBarEvent.NavigateToSettingsScreen -> {
navigateToSettingsGraph(navOptions)
}
+
+ VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen -> {
+ navigateToSettingsGraph(navOptions)
+ }
}
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt
index fc6aa30e2..4b26f2d1d 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt
@@ -65,6 +65,10 @@ class VaultUnlockedNavBarViewModel @Inject constructor(
specialCircumstancesManager.specialCircumstance = null
}
+ SpecialCircumstance.AccountSecurityShortcut -> {
+ sendEvent(VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen)
+ }
+
else -> Unit
}
}
@@ -283,5 +287,12 @@ sealed class VaultUnlockedNavBarEvent {
contentDescriptionRes = contentDescRes,
)
}
+
+ /**
+ * Navigate to the Settings Screen.
+ */
+ data object NavigateToSettingsScreen : Shortcut() {
+ override val tab: VaultUnlockedNavBarTab = VaultUnlockedNavBarTab.Settings()
+ }
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/ShortcutUtils.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/ShortcutUtils.kt
index d55f35379..7ffb005cc 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/ShortcutUtils.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/ShortcutUtils.kt
@@ -13,3 +13,9 @@ val Intent.isMyVaultShortcut: Boolean
*/
val Intent.isPasswordGeneratorShortcut: Boolean
get() = dataString?.equals("bitwarden://password_generator") == true
+
+/**
+ * Returns `true` if the [Intent] is a deeplink to the account security screen, `false` otherwise.
+ */
+val Intent.isAccountSecurityShortcut: Boolean
+ get() = dataString?.equals("bitwarden://settings/account_security") == true
diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
index e23f5008a..b219e288e 100644
--- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
@@ -48,6 +48,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
+import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.model.TotpData
@@ -131,6 +132,7 @@ class MainViewModelTest : BaseViewModelTest() {
mockkStatic(
Intent::isMyVaultShortcut,
Intent::isPasswordGeneratorShortcut,
+ Intent::isAccountSecurityShortcut,
)
}
@@ -146,6 +148,7 @@ class MainViewModelTest : BaseViewModelTest() {
unmockkStatic(
Intent::isMyVaultShortcut,
Intent::isPasswordGeneratorShortcut,
+ Intent::isAccountSecurityShortcut,
)
}
@@ -312,6 +315,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = mockIntent))
assertEquals(
@@ -334,6 +338,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(
MainAction.ReceiveFirstIntent(
@@ -363,6 +368,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(
MainAction.ReceiveFirstIntent(
@@ -394,6 +400,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getAutofillSelectionDataOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { authRepository.activeUserId } returns null
@@ -430,6 +437,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getAutofillSelectionDataOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { authRepository.activeUserId } returns "activeId"
@@ -468,6 +476,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getAutofillSelectionDataOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { authRepository.activeUserId } returns null
@@ -508,6 +517,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getAutofillSelectionDataOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { authRepository.activeUserId } returns null
@@ -550,6 +560,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getAutofillSelectionDataOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { authRepository.activeUserId } returns null
@@ -589,6 +600,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(
MainAction.ReceiveFirstIntent(
@@ -619,6 +631,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(
MainAction.ReceiveFirstIntent(
@@ -706,6 +719,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
coEvery {
@@ -744,6 +758,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
coEvery {
@@ -818,6 +833,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(
MainAction.ReceiveNewIntent(
@@ -847,6 +863,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(MainAction.ReceiveNewIntent(intent = mockIntent))
assertEquals(
@@ -869,6 +886,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(
MainAction.ReceiveNewIntent(
@@ -898,6 +916,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(
MainAction.ReceiveNewIntent(
@@ -928,6 +947,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(
MainAction.ReceiveNewIntent(
@@ -955,6 +975,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns true
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
@@ -969,6 +990,33 @@ class MainViewModelTest : BaseViewModelTest() {
)
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `on ReceiveNewIntent with account security deeplink data should set the special circumstance to AccountSecurityShortcut `() {
+ val viewModel = createViewModel()
+ val mockIntent = mockk {
+ every { getTotpDataOrNull() } returns null
+ every { getPasswordlessRequestDataIntentOrNull() } returns null
+ every { getAutofillSaveItemOrNull() } returns null
+ every { getAutofillSelectionDataOrNull() } returns null
+ every { getCompleteRegistrationDataIntentOrNull() } returns null
+ every { isMyVaultShortcut } returns false
+ every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns true
+ }
+ every { intentManager.getShareDataFromIntent(mockIntent) } returns null
+
+ viewModel.trySendAction(
+ MainAction.ReceiveNewIntent(
+ intent = mockIntent,
+ ),
+ )
+ assertEquals(
+ SpecialCircumstance.AccountSecurityShortcut,
+ specialCircumstanceManager.specialCircumstance,
+ )
+ }
+
@Suppress("MaxLineLength")
@Test
fun `on ReceiveNewIntent with a password generator deeplink data should set the special circumstance to GeneratorShortcut`() {
@@ -981,6 +1029,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns true
+ every { isAccountSecurityShortcut } returns false
}
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
@@ -1070,6 +1119,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false
+ every { mockIntent.isAccountSecurityShortcut } returns false
every { passwordlessRequestData.userId } returns "userId"
viewModel.trySendAction(
@@ -1152,6 +1202,7 @@ private fun createMockFido2RegistrationIntent(
every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
private fun createMockFido2AssertionIntent(
@@ -1166,6 +1217,7 @@ private fun createMockFido2AssertionIntent(
every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
private fun createMockFido2GetCredentialsIntent(
@@ -1183,6 +1235,7 @@ private fun createMockFido2GetCredentialsIntent(
every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
+ every { isAccountSecurityShortcut } returns false
}
private val FIXED_CLOCK: Clock = Clock.fixed(
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 0764fbf84..06908a483 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
@@ -481,6 +481,44 @@ class RootNavViewModelTest : BaseViewModelTest() {
)
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `when the active user has an unlocked vault but there is an AccountSecurityShortcut special circumstance the nav state should be VaultUnlocked`() {
+ specialCircumstanceManager.specialCircumstance =
+ SpecialCircumstance.AccountSecurityShortcut
+ 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(),
+ needsMasterPassword = false,
+ trustedDevice = null,
+ hasMasterPassword = true,
+ isUsingKeyConnector = false,
+ firstTimeState = UserState.FirstTimeState(false),
+ onboardingStatus = OnboardingStatus.COMPLETE,
+ ),
+ ),
+ ),
+ )
+ val viewModel = createViewModel()
+ assertEquals(
+ RootNavState.VaultUnlocked(activeUserId = "activeUserId"),
+ viewModel.stateFlow.value,
+ )
+ }
+
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault but the is a ShareNewSend special circumstance the nav state should be VaultUnlockedForNewSend`() {
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreenTest.kt
index 0b008c26a..61581709e 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreenTest.kt
@@ -260,6 +260,25 @@ class SettingsScreenTest : BaseComposeTest() {
assertTrue(haveCalledNavigateToVault)
}
+ @Test
+ fun `on NavigateAccountSecurityShortcut should call onNavigateToAccountSecurity`() {
+ var haveCalledNavigateToAccountSecurity = false
+ composeTestRule.setContent {
+ SettingsScreen(
+ viewModel = viewModel,
+ onNavigateToAbout = { },
+ onNavigateToAccountSecurity = { haveCalledNavigateToAccountSecurity = true },
+ onNavigateToAppearance = { },
+ onNavigateToAutoFill = { },
+ onNavigateToOther = { },
+ onNavigateToVault = {
+ },
+ )
+ }
+ mutableEventFlow.tryEmit(SettingsEvent.NavigateAccountSecurityShortcut)
+ assertTrue(haveCalledNavigateToAccountSecurity)
+ }
+
@Test
fun `Settings screen should show correct number of notification badges based on state`() {
composeTestRule.setContent {
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt
index cff3ad698..d21b937dd 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt
@@ -1,10 +1,15 @@
package com.x8bit.bitwarden.ui.platform.feature.settings
import app.cash.turbine.test
+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
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
+import io.mockk.just
import io.mockk.mockk
+import io.mockk.runs
+import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest
@@ -19,10 +24,13 @@ class SettingsViewModelTest : BaseViewModelTest() {
every { allSecuritySettingsBadgeCountFlow } returns mutableSecurityBadgeCountFlow
every { allAutofillSettingsBadgeCountFlow } returns mutableAutofillBadgeCountFlow
}
+ private val specialCircumstanceManager: SpecialCircumstanceManager = mockk {
+ every { specialCircumstance } returns null
+ }
@Test
fun `on SettingsClick with ABOUT should emit NavigateAbout`() = runTest {
- val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
+ val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT))
assertEquals(SettingsEvent.NavigateAbout, awaitItem())
@@ -31,7 +39,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test
fun `on SettingsClick with ACCOUNT_SECURITY should emit NavigateAccountSecurity`() = runTest {
- val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
+ val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY))
assertEquals(SettingsEvent.NavigateAccountSecurity, awaitItem())
@@ -40,7 +48,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test
fun `on SettingsClick with APPEARANCE should emit NavigateAppearance`() = runTest {
- val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
+ val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE))
assertEquals(SettingsEvent.NavigateAppearance, awaitItem())
@@ -49,7 +57,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test
fun `on SettingsClick with AUTO_FILL should emit NavigateAutoFill`() = runTest {
- val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
+ val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL))
assertEquals(SettingsEvent.NavigateAutoFill, awaitItem())
@@ -58,7 +66,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test
fun `on SettingsClick with OTHER should emit NavigateOther`() = runTest {
- val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
+ val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER))
assertEquals(SettingsEvent.NavigateOther, awaitItem())
@@ -67,7 +75,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test
fun `on SettingsClick with VAULT should emit NavigateVault`() = runTest {
- val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
+ val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT))
assertEquals(SettingsEvent.NavigateVault, awaitItem())
@@ -78,7 +86,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
fun `initial state reflects the current state of the repository`() {
mutableAutofillBadgeCountFlow.update { 1 }
mutableSecurityBadgeCountFlow.update { 2 }
- val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
+ val viewModel = createViewModel()
assertEquals(
SettingsState(
autoFillCount = 1,
@@ -90,7 +98,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test
fun `State updates when repository emits new values for badge counts`() = runTest {
- val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
+ val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
SettingsState(
@@ -119,4 +127,25 @@ class SettingsViewModelTest : BaseViewModelTest() {
)
}
}
+
+ @Test
+ @Suppress("MaxLineLength")
+ fun `init should send NavigateAccountSecurityShortcut when special circumstance is AccountSecurityShortcut`() =
+ runTest {
+ every {
+ specialCircumstanceManager.specialCircumstance
+ } returns SpecialCircumstance.AccountSecurityShortcut
+ every { specialCircumstanceManager.specialCircumstance = null } just runs
+ createViewModel().eventFlow.test {
+ assertEquals(
+ SettingsEvent.NavigateAccountSecurityShortcut, awaitItem(),
+ )
+ }
+ verify { specialCircumstanceManager.specialCircumstance = null }
+ }
+
+ private fun createViewModel() = SettingsViewModel(
+ settingsRepository = settingsRepository,
+ specialCircumstanceManager = specialCircumstanceManager,
+ )
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt
index 9f4f07db9..0999c3b57 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt
@@ -103,6 +103,21 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
}
}
+ @Test
+ fun `NavigateToSettingsScreen shortcut event should navigate to SettingsScreen`() {
+ mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen)
+ composeTestRule.runOnIdle { fakeNavHostController.assertCurrentRoute("send_graph") }
+ mutableEventFlow.tryEmit(
+ VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen,
+ )
+ composeTestRule.runOnIdle {
+ fakeNavHostController.assertLastNavigation(
+ route = "settings_graph",
+ navOptions = expectedNavOptions,
+ )
+ }
+ }
+
@Test
fun `send tab click should send SendTabClick action`() {
composeTestRule.onNodeWithText("Send").performClick()
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt
index 05fbaf24c..f658b8474 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt
@@ -80,6 +80,27 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() {
}
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `on init with AccountSecurityShortcut special circumstance should navigate to the settings screen with shortcut event`() =
+ runTest {
+ every {
+ specialCircumstancesManager.specialCircumstance
+ } returns SpecialCircumstance.AccountSecurityShortcut
+
+ val viewModel = createViewModel()
+
+ viewModel.eventFlow.test {
+ assertEquals(
+ VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen,
+ awaitItem(),
+ )
+ }
+ verify(exactly = 1) {
+ specialCircumstancesManager.specialCircumstance
+ }
+ }
+
@Test
fun `on init with no shortcut special circumstance should do nothing`() = runTest {
every { specialCircumstancesManager.specialCircumstance } returns null