mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
PM-11256: Add RootNav logic to display Remove Password Screen (#3803)
This commit is contained in:
parent
075956ce17
commit
e7bd966e94
4 changed files with 119 additions and 12 deletions
|
@ -37,4 +37,10 @@ data class JwtTokenDataJson(
|
||||||
|
|
||||||
@SerialName("amr")
|
@SerialName("amr")
|
||||||
val authenticationMethodsReference: List<String>,
|
val authenticationMethodsReference: List<String>,
|
||||||
)
|
) {
|
||||||
|
/**
|
||||||
|
* Indicates that this is an external user. Mainly used for SSO users with a key connector.
|
||||||
|
*/
|
||||||
|
val isExternal: Boolean
|
||||||
|
get() = authenticationMethodsReference.any { it == "external" }
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,9 @@ import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.auth.authGraph
|
import com.x8bit.bitwarden.ui.auth.feature.auth.authGraph
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph
|
import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
|
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.removepassword.REMOVE_PASSWORD_ROUTE
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.removepassword.navigateToRemovePassword
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.removepassword.removePasswordDestination
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.RESET_PASSWORD_ROUTE
|
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.RESET_PASSWORD_ROUTE
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.navigateToResetPasswordGraph
|
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.navigateToResetPasswordGraph
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.resetPasswordDestination
|
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.resetPasswordDestination
|
||||||
|
@ -79,6 +82,7 @@ fun RootNavScreen(
|
||||||
) {
|
) {
|
||||||
splashDestination()
|
splashDestination()
|
||||||
authGraph(navController)
|
authGraph(navController)
|
||||||
|
removePasswordDestination()
|
||||||
resetPasswordDestination()
|
resetPasswordDestination()
|
||||||
trustedDeviceGraph(navController)
|
trustedDeviceGraph(navController)
|
||||||
vaultUnlockDestination()
|
vaultUnlockDestination()
|
||||||
|
@ -93,6 +97,7 @@ fun RootNavScreen(
|
||||||
|
|
||||||
RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE
|
RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE
|
||||||
RootNavState.SetPassword -> SET_PASSWORD_ROUTE
|
RootNavState.SetPassword -> SET_PASSWORD_ROUTE
|
||||||
|
RootNavState.RemovePassword -> REMOVE_PASSWORD_ROUTE
|
||||||
RootNavState.Splash -> SPLASH_ROUTE
|
RootNavState.Splash -> SPLASH_ROUTE
|
||||||
RootNavState.TrustedDevice -> TRUSTED_DEVICE_GRAPH_ROUTE
|
RootNavState.TrustedDevice -> TRUSTED_DEVICE_GRAPH_ROUTE
|
||||||
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
|
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
|
||||||
|
@ -149,13 +154,14 @@ fun RootNavScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RootNavState.RemovePassword -> navController.navigateToRemovePassword(rootNavOptions)
|
||||||
RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions)
|
RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions)
|
||||||
RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
|
RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
|
||||||
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
||||||
RootNavState.TrustedDevice -> navController.navigateToTrustedDeviceGraph(rootNavOptions)
|
RootNavState.TrustedDevice -> navController.navigateToTrustedDeviceGraph(rootNavOptions)
|
||||||
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
|
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
|
||||||
is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(
|
is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(
|
||||||
rootNavOptions,
|
navOptions = rootNavOptions,
|
||||||
)
|
)
|
||||||
|
|
||||||
RootNavState.VaultUnlockedForNewSend -> {
|
RootNavState.VaultUnlockedForNewSend -> {
|
||||||
|
|
|
@ -3,7 +3,9 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.util.parseJwtTokenDataOrNull
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||||
|
@ -11,6 +13,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
@ -32,12 +35,12 @@ class RootNavViewModel @Inject constructor(
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
combine(
|
combine(
|
||||||
authRepository
|
authRepository.authStateFlow,
|
||||||
.userStateFlow,
|
authRepository.userStateFlow,
|
||||||
specialCircumstanceManager
|
specialCircumstanceManager.specialCircumstanceStateFlow,
|
||||||
.specialCircumstanceStateFlow,
|
) { authState, userState, specialCircumstance ->
|
||||||
) { userState, specialCircumstance ->
|
|
||||||
RootNavAction.Internal.UserStateUpdateReceive(
|
RootNavAction.Internal.UserStateUpdateReceive(
|
||||||
|
authState = authState,
|
||||||
userState = userState,
|
userState = userState,
|
||||||
specialCircumstance = specialCircumstance,
|
specialCircumstance = specialCircumstance,
|
||||||
)
|
)
|
||||||
|
@ -86,6 +89,11 @@ class RootNavViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userState.activeAccount.isVaultUnlocked &&
|
||||||
|
userState.shouldShowRemovePassword(authState = action.authState) -> {
|
||||||
|
RootNavState.RemovePassword
|
||||||
|
}
|
||||||
|
|
||||||
userState.activeAccount.isVaultUnlocked -> {
|
userState.activeAccount.isVaultUnlocked -> {
|
||||||
when (specialCircumstance) {
|
when (specialCircumstance) {
|
||||||
is SpecialCircumstance.AutofillSave -> {
|
is SpecialCircumstance.AutofillSave -> {
|
||||||
|
@ -145,6 +153,20 @@ class RootNavViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
mutableStateFlow.update { updatedRootNavState }
|
mutableStateFlow.update { updatedRootNavState }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun UserState.shouldShowRemovePassword(authState: AuthState): Boolean {
|
||||||
|
val isLoggedInUsingSso = (authState as? AuthState.Authenticated)
|
||||||
|
?.accessToken
|
||||||
|
?.let(::parseJwtTokenDataOrNull)
|
||||||
|
?.isExternal == true
|
||||||
|
val usesKeyConnectorAndNotAdmin = this.activeAccount.organizations.any {
|
||||||
|
it.shouldUseKeyConnector &&
|
||||||
|
it.role != OrganizationType.OWNER &&
|
||||||
|
it.role != OrganizationType.ADMIN
|
||||||
|
}
|
||||||
|
val userIsNotUsingKeyConnector = !this.activeAccount.isUsingKeyConnector
|
||||||
|
return isLoggedInUsingSso && usesKeyConnectorAndNotAdmin && userIsNotUsingKeyConnector
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -163,6 +185,12 @@ sealed class RootNavState : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object AuthWithWelcome : RootNavState()
|
data object AuthWithWelcome : RootNavState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App should show remove password graph.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object RemovePassword : RootNavState()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App should show reset password graph.
|
* App should show reset password graph.
|
||||||
*/
|
*/
|
||||||
|
@ -290,6 +318,7 @@ sealed class RootNavAction {
|
||||||
* User state in the repository layer changed.
|
* User state in the repository layer changed.
|
||||||
*/
|
*/
|
||||||
data class UserStateUpdateReceive(
|
data class UserStateUpdateReceive(
|
||||||
|
val authState: AuthState,
|
||||||
val userState: UserState?,
|
val userState: UserState?,
|
||||||
val specialCircumstance: SpecialCircumstance?,
|
val specialCircumstance: SpecialCircumstance?,
|
||||||
) : RootNavAction()
|
) : RootNavAction()
|
||||||
|
|
|
@ -2,7 +2,11 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||||
|
|
||||||
import android.content.pm.SigningInfo
|
import android.content.pm.SigningInfo
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.util.parseJwtTokenDataOrNull
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest
|
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest
|
||||||
|
@ -12,11 +16,16 @@ import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||||
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.mockk
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -24,13 +33,25 @@ import java.time.ZoneOffset
|
||||||
|
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
class RootNavViewModelTest : BaseViewModelTest() {
|
class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
|
private val mutableAuthStateFlow = MutableStateFlow<AuthState>(AuthState.Uninitialized)
|
||||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
|
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
|
||||||
private val authRepository = mockk<AuthRepository> {
|
private val authRepository = mockk<AuthRepository> {
|
||||||
every { userStateFlow } returns mutableUserStateFlow
|
every { userStateFlow } returns mutableUserStateFlow
|
||||||
|
every { authStateFlow } returns mutableAuthStateFlow
|
||||||
every { showWelcomeCarousel } returns false
|
every { showWelcomeCarousel } returns false
|
||||||
}
|
}
|
||||||
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
|
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
mockkStatic(::parseJwtTokenDataOrNull)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic(::parseJwtTokenDataOrNull)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when there are no accounts the nav state should be Auth`() {
|
fun `when there are no accounts the nav state should be Auth`() {
|
||||||
mutableUserStateFlow.tryEmit(null)
|
mutableUserStateFlow.tryEmit(null)
|
||||||
|
@ -294,6 +315,49 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `when the active user has an unlocked vault with an external user, a key-connector user account and is not currently using key connector the nav state should be RemovePassword`() {
|
||||||
|
val jwtTokenDataJson = mockk<JwtTokenDataJson> {
|
||||||
|
every { isExternal } returns true
|
||||||
|
}
|
||||||
|
every { parseJwtTokenDataOrNull(ACCESS_TOKEN) } returns jwtTokenDataJson
|
||||||
|
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 = listOf(
|
||||||
|
Organization(
|
||||||
|
id = "orgId",
|
||||||
|
name = "orgName",
|
||||||
|
shouldUseKeyConnector = true,
|
||||||
|
role = OrganizationType.USER,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
needsMasterPassword = false,
|
||||||
|
trustedDevice = null,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
isUsingKeyConnector = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mutableAuthStateFlow.value = AuthState.Authenticated(accessToken = ACCESS_TOKEN)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(RootNavState.RemovePassword, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when the active user has an unlocked vault the nav state should be VaultUnlocked`() {
|
fun `when the active user has an unlocked vault the nav state should be VaultUnlocked`() {
|
||||||
mutableUserStateFlow.tryEmit(
|
mutableUserStateFlow.tryEmit(
|
||||||
|
@ -742,9 +806,11 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
authRepository = authRepository,
|
authRepository = authRepository,
|
||||||
specialCircumstanceManager = specialCircumstanceManager,
|
specialCircumstanceManager = specialCircumstanceManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
|
||||||
Instant.parse("2023-10-27T12:00:00Z"),
|
|
||||||
ZoneOffset.UTC,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||||
|
Instant.parse("2023-10-27T12:00:00Z"),
|
||||||
|
ZoneOffset.UTC,
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val ACCESS_TOKEN: String = "access_token"
|
||||||
|
|
Loading…
Reference in a new issue