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")
|
||||
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.navigateToAuthGraph
|
||||
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.navigateToResetPasswordGraph
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.resetPasswordDestination
|
||||
|
@ -79,6 +82,7 @@ fun RootNavScreen(
|
|||
) {
|
||||
splashDestination()
|
||||
authGraph(navController)
|
||||
removePasswordDestination()
|
||||
resetPasswordDestination()
|
||||
trustedDeviceGraph(navController)
|
||||
vaultUnlockDestination()
|
||||
|
@ -93,6 +97,7 @@ fun RootNavScreen(
|
|||
|
||||
RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE
|
||||
RootNavState.SetPassword -> SET_PASSWORD_ROUTE
|
||||
RootNavState.RemovePassword -> REMOVE_PASSWORD_ROUTE
|
||||
RootNavState.Splash -> SPLASH_ROUTE
|
||||
RootNavState.TrustedDevice -> TRUSTED_DEVICE_GRAPH_ROUTE
|
||||
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
|
||||
|
@ -149,13 +154,14 @@ fun RootNavScreen(
|
|||
)
|
||||
}
|
||||
|
||||
RootNavState.RemovePassword -> navController.navigateToRemovePassword(rootNavOptions)
|
||||
RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions)
|
||||
RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
|
||||
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
||||
RootNavState.TrustedDevice -> navController.navigateToTrustedDeviceGraph(rootNavOptions)
|
||||
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
|
||||
is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(
|
||||
rootNavOptions,
|
||||
navOptions = rootNavOptions,
|
||||
)
|
||||
|
||||
RootNavState.VaultUnlockedForNewSend -> {
|
||||
|
|
|
@ -3,7 +3,9 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.util.parseJwtTokenDataOrNull
|
||||
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.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.platform.manager.SpecialCircumstanceManager
|
||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
@ -32,12 +35,12 @@ class RootNavViewModel @Inject constructor(
|
|||
) {
|
||||
init {
|
||||
combine(
|
||||
authRepository
|
||||
.userStateFlow,
|
||||
specialCircumstanceManager
|
||||
.specialCircumstanceStateFlow,
|
||||
) { userState, specialCircumstance ->
|
||||
authRepository.authStateFlow,
|
||||
authRepository.userStateFlow,
|
||||
specialCircumstanceManager.specialCircumstanceStateFlow,
|
||||
) { authState, userState, specialCircumstance ->
|
||||
RootNavAction.Internal.UserStateUpdateReceive(
|
||||
authState = authState,
|
||||
userState = userState,
|
||||
specialCircumstance = specialCircumstance,
|
||||
)
|
||||
|
@ -86,6 +89,11 @@ class RootNavViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
userState.activeAccount.isVaultUnlocked &&
|
||||
userState.shouldShowRemovePassword(authState = action.authState) -> {
|
||||
RootNavState.RemovePassword
|
||||
}
|
||||
|
||||
userState.activeAccount.isVaultUnlocked -> {
|
||||
when (specialCircumstance) {
|
||||
is SpecialCircumstance.AutofillSave -> {
|
||||
|
@ -145,6 +153,20 @@ class RootNavViewModel @Inject constructor(
|
|||
}
|
||||
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
|
||||
data object AuthWithWelcome : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show remove password graph.
|
||||
*/
|
||||
@Parcelize
|
||||
data object RemovePassword : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show reset password graph.
|
||||
*/
|
||||
|
@ -290,6 +318,7 @@ sealed class RootNavAction {
|
|||
* User state in the repository layer changed.
|
||||
*/
|
||||
data class UserStateUpdateReceive(
|
||||
val authState: AuthState,
|
||||
val userState: UserState?,
|
||||
val specialCircumstance: SpecialCircumstance?,
|
||||
) : RootNavAction()
|
||||
|
|
|
@ -2,7 +2,11 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
|||
|
||||
import android.content.pm.SigningInfo
|
||||
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.util.parseJwtTokenDataOrNull
|
||||
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.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.SpecialCircumstance
|
||||
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 io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
|
@ -24,13 +33,25 @@ import java.time.ZoneOffset
|
|||
|
||||
@Suppress("LargeClass")
|
||||
class RootNavViewModelTest : BaseViewModelTest() {
|
||||
private val mutableAuthStateFlow = MutableStateFlow<AuthState>(AuthState.Uninitialized)
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { authStateFlow } returns mutableAuthStateFlow
|
||||
every { showWelcomeCarousel } returns false
|
||||
}
|
||||
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(::parseJwtTokenDataOrNull)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(::parseJwtTokenDataOrNull)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when there are no accounts the nav state should be Auth`() {
|
||||
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
|
||||
fun `when the active user has an unlocked vault the nav state should be VaultUnlocked`() {
|
||||
mutableUserStateFlow.tryEmit(
|
||||
|
@ -742,9 +806,11 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
authRepository = authRepository,
|
||||
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