BITAU-105 Add support for deep link to account security (#4063)

This commit is contained in:
Andrew Haisting 2024-10-16 09:45:10 -05:00 committed by GitHub
parent 43dc2f8116
commit 1446e43c46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 243 additions and 9 deletions

View file

@ -98,6 +98,11 @@
<data android:scheme="otpauth" /> <data android:scheme="otpauth" />
<data android:host="totp" /> <data android:host="totp" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="bitwarden" />
</intent-filter>
</activity> </activity>
<activity <activity

View file

@ -30,6 +30,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText 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.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager 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.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
@ -236,6 +237,7 @@ class MainViewModel @Inject constructor(
val totpData = intent.getTotpDataOrNull() val totpData = intent.getTotpDataOrNull()
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
val hasVaultShortcut = intent.isMyVaultShortcut val hasVaultShortcut = intent.isMyVaultShortcut
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull() val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull() val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull() val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
@ -330,6 +332,11 @@ class MainViewModel @Inject constructor(
hasVaultShortcut -> { hasVaultShortcut -> {
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.VaultShortcut specialCircumstanceManager.specialCircumstance = SpecialCircumstance.VaultShortcut
} }
hasAccountSecurityShortcut -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AccountSecurityShortcut
}
} }
} }

View file

@ -98,6 +98,12 @@ sealed class SpecialCircumstance : Parcelable {
@Parcelize @Parcelize
data object VaultShortcut : SpecialCircumstance() 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 * A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
* cleared after a successful login. * cleared after a successful login.

View file

@ -151,6 +151,7 @@ class RootNavViewModel @Inject constructor(
) )
} }
SpecialCircumstance.AccountSecurityShortcut,
SpecialCircumstance.GeneratorShortcut, SpecialCircumstance.GeneratorShortcut,
SpecialCircumstance.VaultShortcut, SpecialCircumstance.VaultShortcut,
null, null,

View file

@ -67,6 +67,7 @@ fun SettingsScreen(
SettingsEvent.NavigateAutoFill -> onNavigateToAutoFill() SettingsEvent.NavigateAutoFill -> onNavigateToAutoFill()
SettingsEvent.NavigateOther -> onNavigateToOther() SettingsEvent.NavigateOther -> onNavigateToOther()
SettingsEvent.NavigateVault -> onNavigateToVault() SettingsEvent.NavigateVault -> onNavigateToVault()
SettingsEvent.NavigateAccountSecurityShortcut -> onNavigateToAccountSecurity()
} }
} }

View file

@ -3,8 +3,11 @@ package com.x8bit.bitwarden.ui.platform.feature.settings
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R 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.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel 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.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -20,6 +23,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
settingsRepository: SettingsRepository, settingsRepository: SettingsRepository,
specialCircumstanceManager: SpecialCircumstanceManager,
) : BaseViewModel<SettingsState, SettingsEvent, SettingsAction>( ) : BaseViewModel<SettingsState, SettingsEvent, SettingsAction>(
initialState = SettingsState( initialState = SettingsState(
securityCount = settingsRepository.allSecuritySettingsBadgeCountFlow.value, securityCount = settingsRepository.allSecuritySettingsBadgeCountFlow.value,
@ -39,6 +43,15 @@ class SettingsViewModel @Inject constructor(
} }
.onEach(::sendAction) .onEach(::sendAction)
.launchIn(viewModelScope) .launchIn(viewModelScope)
when (specialCircumstanceManager.specialCircumstance) {
SpecialCircumstance.AccountSecurityShortcut -> {
sendEvent(SettingsEvent.NavigateAccountSecurityShortcut)
specialCircumstanceManager.specialCircumstance = null
}
else -> Unit
}
} }
override fun handleAction(action: SettingsAction): Unit = when (action) { override fun handleAction(action: SettingsAction): Unit = when (action) {
@ -115,6 +128,11 @@ sealed class SettingsEvent {
*/ */
data object NavigateAccountSecurity : SettingsEvent() data object NavigateAccountSecurity : SettingsEvent()
/**
* Navigate to the account security screen.
*/
data object NavigateAccountSecurityShortcut : SettingsEvent(), BackgroundEvent
/** /**
* Navigate to the appearance screen. * Navigate to the appearance screen.
*/ */

View file

@ -58,7 +58,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
/** /**
* Top level composable for the Vault Unlocked Screen. * Top level composable for the Vault Unlocked Screen.
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList", "LongMethod")
@Composable @Composable
fun VaultUnlockedNavBarScreen( fun VaultUnlockedNavBarScreen(
viewModel: VaultUnlockedNavBarViewModel = hiltViewModel(), viewModel: VaultUnlockedNavBarViewModel = hiltViewModel(),
@ -104,6 +104,10 @@ fun VaultUnlockedNavBarScreen(
VaultUnlockedNavBarEvent.NavigateToSettingsScreen -> { VaultUnlockedNavBarEvent.NavigateToSettingsScreen -> {
navigateToSettingsGraph(navOptions) navigateToSettingsGraph(navOptions)
} }
VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen -> {
navigateToSettingsGraph(navOptions)
}
} }
} }
} }

View file

@ -65,6 +65,10 @@ class VaultUnlockedNavBarViewModel @Inject constructor(
specialCircumstancesManager.specialCircumstance = null specialCircumstancesManager.specialCircumstance = null
} }
SpecialCircumstance.AccountSecurityShortcut -> {
sendEvent(VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen)
}
else -> Unit else -> Unit
} }
} }
@ -283,5 +287,12 @@ sealed class VaultUnlockedNavBarEvent {
contentDescriptionRes = contentDescRes, contentDescriptionRes = contentDescRes,
) )
} }
/**
* Navigate to the Settings Screen.
*/
data object NavigateToSettingsScreen : Shortcut() {
override val tab: VaultUnlockedNavBarTab = VaultUnlockedNavBarTab.Settings()
}
} }
} }

View file

@ -13,3 +13,9 @@ val Intent.isMyVaultShortcut: Boolean
*/ */
val Intent.isPasswordGeneratorShortcut: Boolean val Intent.isPasswordGeneratorShortcut: Boolean
get() = dataString?.equals("bitwarden://password_generator") == true 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

View file

@ -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.base.util.asText
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme 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.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.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.model.TotpData import com.x8bit.bitwarden.ui.vault.model.TotpData
@ -131,6 +132,7 @@ class MainViewModelTest : BaseViewModelTest() {
mockkStatic( mockkStatic(
Intent::isMyVaultShortcut, Intent::isMyVaultShortcut,
Intent::isPasswordGeneratorShortcut, Intent::isPasswordGeneratorShortcut,
Intent::isAccountSecurityShortcut,
) )
} }
@ -146,6 +148,7 @@ class MainViewModelTest : BaseViewModelTest() {
unmockkStatic( unmockkStatic(
Intent::isMyVaultShortcut, Intent::isMyVaultShortcut,
Intent::isPasswordGeneratorShortcut, Intent::isPasswordGeneratorShortcut,
Intent::isAccountSecurityShortcut,
) )
} }
@ -312,6 +315,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = mockIntent)) viewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = mockIntent))
assertEquals( assertEquals(
@ -334,6 +338,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction( viewModel.trySendAction(
MainAction.ReceiveFirstIntent( MainAction.ReceiveFirstIntent(
@ -363,6 +368,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction( viewModel.trySendAction(
MainAction.ReceiveFirstIntent( MainAction.ReceiveFirstIntent(
@ -394,6 +400,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { authRepository.activeUserId } returns null every { authRepository.activeUserId } returns null
@ -430,6 +437,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { authRepository.activeUserId } returns "activeId" every { authRepository.activeUserId } returns "activeId"
@ -468,6 +476,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { authRepository.activeUserId } returns null every { authRepository.activeUserId } returns null
@ -508,6 +517,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { authRepository.activeUserId } returns null every { authRepository.activeUserId } returns null
@ -550,6 +560,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { authRepository.activeUserId } returns null every { authRepository.activeUserId } returns null
@ -589,6 +600,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction( viewModel.trySendAction(
MainAction.ReceiveFirstIntent( MainAction.ReceiveFirstIntent(
@ -619,6 +631,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction( viewModel.trySendAction(
MainAction.ReceiveFirstIntent( MainAction.ReceiveFirstIntent(
@ -706,6 +719,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getCompleteRegistrationDataIntentOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
coEvery { coEvery {
@ -744,6 +758,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getCompleteRegistrationDataIntentOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
coEvery { coEvery {
@ -818,6 +833,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction( viewModel.trySendAction(
MainAction.ReceiveNewIntent( MainAction.ReceiveNewIntent(
@ -847,6 +863,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction(MainAction.ReceiveNewIntent(intent = mockIntent)) viewModel.trySendAction(MainAction.ReceiveNewIntent(intent = mockIntent))
assertEquals( assertEquals(
@ -869,6 +886,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction( viewModel.trySendAction(
MainAction.ReceiveNewIntent( MainAction.ReceiveNewIntent(
@ -898,6 +916,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction( viewModel.trySendAction(
MainAction.ReceiveNewIntent( MainAction.ReceiveNewIntent(
@ -928,6 +947,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
viewModel.trySendAction( viewModel.trySendAction(
MainAction.ReceiveNewIntent( MainAction.ReceiveNewIntent(
@ -955,6 +975,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { getCompleteRegistrationDataIntentOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns true every { isMyVaultShortcut } returns true
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
every { intentManager.getShareDataFromIntent(mockIntent) } returns null 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<Intent> {
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") @Suppress("MaxLineLength")
@Test @Test
fun `on ReceiveNewIntent with a password generator deeplink data should set the special circumstance to GeneratorShortcut`() { 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 { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns true every { isPasswordGeneratorShortcut } returns true
every { isAccountSecurityShortcut } returns false
} }
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
@ -1070,6 +1119,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false
every { mockIntent.isAccountSecurityShortcut } returns false
every { passwordlessRequestData.userId } returns "userId" every { passwordlessRequestData.userId } returns "userId"
viewModel.trySendAction( viewModel.trySendAction(
@ -1152,6 +1202,7 @@ private fun createMockFido2RegistrationIntent(
every { getCompleteRegistrationDataIntentOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
private fun createMockFido2AssertionIntent( private fun createMockFido2AssertionIntent(
@ -1166,6 +1217,7 @@ private fun createMockFido2AssertionIntent(
every { getCompleteRegistrationDataIntentOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
private fun createMockFido2GetCredentialsIntent( private fun createMockFido2GetCredentialsIntent(
@ -1183,6 +1235,7 @@ private fun createMockFido2GetCredentialsIntent(
every { getCompleteRegistrationDataIntentOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns null
every { isMyVaultShortcut } returns false every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false every { isPasswordGeneratorShortcut } returns false
every { isAccountSecurityShortcut } returns false
} }
private val FIXED_CLOCK: Clock = Clock.fixed( private val FIXED_CLOCK: Clock = Clock.fixed(

View file

@ -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") @Suppress("MaxLineLength")
@Test @Test
fun `when the active user has an unlocked vault but the is a ShareNewSend special circumstance the nav state should be VaultUnlockedForNewSend`() { fun `when the active user has an unlocked vault but the is a ShareNewSend special circumstance the nav state should be VaultUnlockedForNewSend`() {

View file

@ -260,6 +260,25 @@ class SettingsScreenTest : BaseComposeTest() {
assertTrue(haveCalledNavigateToVault) 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 @Test
fun `Settings screen should show correct number of notification badges based on state`() { fun `Settings screen should show correct number of notification badges based on state`() {
composeTestRule.setContent { composeTestRule.setContent {

View file

@ -1,10 +1,15 @@
package com.x8bit.bitwarden.ui.platform.feature.settings package com.x8bit.bitwarden.ui.platform.feature.settings
import app.cash.turbine.test 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.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -19,10 +24,13 @@ class SettingsViewModelTest : BaseViewModelTest() {
every { allSecuritySettingsBadgeCountFlow } returns mutableSecurityBadgeCountFlow every { allSecuritySettingsBadgeCountFlow } returns mutableSecurityBadgeCountFlow
every { allAutofillSettingsBadgeCountFlow } returns mutableAutofillBadgeCountFlow every { allAutofillSettingsBadgeCountFlow } returns mutableAutofillBadgeCountFlow
} }
private val specialCircumstanceManager: SpecialCircumstanceManager = mockk {
every { specialCircumstance } returns null
}
@Test @Test
fun `on SettingsClick with ABOUT should emit NavigateAbout`() = runTest { fun `on SettingsClick with ABOUT should emit NavigateAbout`() = runTest {
val viewModel = SettingsViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT))
assertEquals(SettingsEvent.NavigateAbout, awaitItem()) assertEquals(SettingsEvent.NavigateAbout, awaitItem())
@ -31,7 +39,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `on SettingsClick with ACCOUNT_SECURITY should emit NavigateAccountSecurity`() = runTest { fun `on SettingsClick with ACCOUNT_SECURITY should emit NavigateAccountSecurity`() = runTest {
val viewModel = SettingsViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY))
assertEquals(SettingsEvent.NavigateAccountSecurity, awaitItem()) assertEquals(SettingsEvent.NavigateAccountSecurity, awaitItem())
@ -40,7 +48,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `on SettingsClick with APPEARANCE should emit NavigateAppearance`() = runTest { fun `on SettingsClick with APPEARANCE should emit NavigateAppearance`() = runTest {
val viewModel = SettingsViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE))
assertEquals(SettingsEvent.NavigateAppearance, awaitItem()) assertEquals(SettingsEvent.NavigateAppearance, awaitItem())
@ -49,7 +57,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `on SettingsClick with AUTO_FILL should emit NavigateAutoFill`() = runTest { fun `on SettingsClick with AUTO_FILL should emit NavigateAutoFill`() = runTest {
val viewModel = SettingsViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL))
assertEquals(SettingsEvent.NavigateAutoFill, awaitItem()) assertEquals(SettingsEvent.NavigateAutoFill, awaitItem())
@ -58,7 +66,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `on SettingsClick with OTHER should emit NavigateOther`() = runTest { fun `on SettingsClick with OTHER should emit NavigateOther`() = runTest {
val viewModel = SettingsViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER))
assertEquals(SettingsEvent.NavigateOther, awaitItem()) assertEquals(SettingsEvent.NavigateOther, awaitItem())
@ -67,7 +75,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `on SettingsClick with VAULT should emit NavigateVault`() = runTest { fun `on SettingsClick with VAULT should emit NavigateVault`() = runTest {
val viewModel = SettingsViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT))
assertEquals(SettingsEvent.NavigateVault, awaitItem()) assertEquals(SettingsEvent.NavigateVault, awaitItem())
@ -78,7 +86,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
fun `initial state reflects the current state of the repository`() { fun `initial state reflects the current state of the repository`() {
mutableAutofillBadgeCountFlow.update { 1 } mutableAutofillBadgeCountFlow.update { 1 }
mutableSecurityBadgeCountFlow.update { 2 } mutableSecurityBadgeCountFlow.update { 2 }
val viewModel = SettingsViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel()
assertEquals( assertEquals(
SettingsState( SettingsState(
autoFillCount = 1, autoFillCount = 1,
@ -90,7 +98,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `State updates when repository emits new values for badge counts`() = runTest { fun `State updates when repository emits new values for badge counts`() = runTest {
val viewModel = SettingsViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel()
viewModel.stateFlow.test { viewModel.stateFlow.test {
assertEquals( assertEquals(
SettingsState( 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,
)
} }

View file

@ -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 @Test
fun `send tab click should send SendTabClick action`() { fun `send tab click should send SendTabClick action`() {
composeTestRule.onNodeWithText("Send").performClick() composeTestRule.onNodeWithText("Send").performClick()

View file

@ -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 @Test
fun `on init with no shortcut special circumstance should do nothing`() = runTest { fun `on init with no shortcut special circumstance should do nothing`() = runTest {
every { specialCircumstancesManager.specialCircumstance } returns null every { specialCircumstancesManager.specialCircumstance } returns null