mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
Add login with password flow (#1254)
This commit is contained in:
parent
f2301e15b9
commit
44728bba02
14 changed files with 270 additions and 50 deletions
|
@ -373,13 +373,15 @@ class AuthRepositoryImpl(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
privateKey = keys.privateKey,
|
privateKey = keys.privateKey,
|
||||||
)
|
)
|
||||||
|
// Order matters here, we need to make sure that the vault is unlocked
|
||||||
|
// before we trust the device, to avoid state-base navigation issues.
|
||||||
|
vaultRepository.syncVaultState(userId = userId)
|
||||||
keys.deviceKey?.let { trustDeviceResponse ->
|
keys.deviceKey?.let { trustDeviceResponse ->
|
||||||
trustedDeviceManager.trustThisDevice(
|
trustedDeviceManager.trustThisDevice(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
trustDeviceResponse = trustDeviceResponse,
|
trustDeviceResponse = trustDeviceResponse,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
vaultRepository.syncVaultState(userId = userId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fold(
|
.fold(
|
||||||
|
|
|
@ -10,6 +10,8 @@ import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDevice
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.navigateToLoginWithDevice
|
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.navigateToLoginWithDevice
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLogin
|
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLogin
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestination
|
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestination
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToTdeVaultUnlock
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.tdeVaultUnlockDestination
|
||||||
|
|
||||||
const val TRUSTED_DEVICE_GRAPH_ROUTE: String = "trusted_device_graph"
|
const val TRUSTED_DEVICE_GRAPH_ROUTE: String = "trusted_device_graph"
|
||||||
|
|
||||||
|
@ -43,7 +45,11 @@ fun NavGraphBuilder.trustedDeviceGraph(navController: NavHostController) {
|
||||||
loginType = LoginWithDeviceType.SSO_OTHER_DEVICE,
|
loginType = LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onNavigateToLock = {
|
||||||
|
navController.navigateToTdeVaultUnlock()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
tdeVaultUnlockDestination()
|
||||||
twoFactorLoginDestination(
|
twoFactorLoginDestination(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,7 @@ const val TRUSTED_DEVICE_ROUTE: String = "trusted_device"
|
||||||
fun NavGraphBuilder.trustedDeviceDestination(
|
fun NavGraphBuilder.trustedDeviceDestination(
|
||||||
onNavigateToAdminApproval: (emailAddress: String) -> Unit,
|
onNavigateToAdminApproval: (emailAddress: String) -> Unit,
|
||||||
onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit,
|
onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit,
|
||||||
|
onNavigateToLock: (emailAddress: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions(
|
composableWithSlideTransitions(
|
||||||
route = TRUSTED_DEVICE_ROUTE,
|
route = TRUSTED_DEVICE_ROUTE,
|
||||||
|
@ -23,6 +24,7 @@ fun NavGraphBuilder.trustedDeviceDestination(
|
||||||
TrustedDeviceScreen(
|
TrustedDeviceScreen(
|
||||||
onNavigateToAdminApproval = onNavigateToAdminApproval,
|
onNavigateToAdminApproval = onNavigateToAdminApproval,
|
||||||
onNavigateToLoginWithOtherDevice = onNavigateToLoginWithOtherDevice,
|
onNavigateToLoginWithOtherDevice = onNavigateToLoginWithOtherDevice,
|
||||||
|
onNavigateToLock = onNavigateToLock,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||||
fun TrustedDeviceScreen(
|
fun TrustedDeviceScreen(
|
||||||
onNavigateToAdminApproval: (emailAddress: String) -> Unit,
|
onNavigateToAdminApproval: (emailAddress: String) -> Unit,
|
||||||
onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit,
|
onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit,
|
||||||
|
onNavigateToLock: (emailAddress: String) -> Unit,
|
||||||
viewModel: TrustedDeviceViewModel = hiltViewModel(),
|
viewModel: TrustedDeviceViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
@ -68,6 +69,10 @@ fun TrustedDeviceScreen(
|
||||||
onNavigateToLoginWithOtherDevice(event.email)
|
onNavigateToLoginWithOtherDevice(event.email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is TrustedDeviceEvent.NavigateToLockScreen -> {
|
||||||
|
onNavigateToLock(event.email)
|
||||||
|
}
|
||||||
|
|
||||||
is TrustedDeviceEvent.ShowToast -> {
|
is TrustedDeviceEvent.ShowToast -> {
|
||||||
Toast
|
Toast
|
||||||
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
|
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
|
||||||
|
|
|
@ -129,7 +129,8 @@ class TrustedDeviceViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleApproveWithPasswordClick() {
|
private fun handleApproveWithPasswordClick() {
|
||||||
sendEvent(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()))
|
authRepository.shouldTrustDevice = state.isRemembered
|
||||||
|
sendEvent(TrustedDeviceEvent.NavigateToLockScreen(state.emailAddress))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNotYouClick() {
|
private fun handleNotYouClick() {
|
||||||
|
@ -192,6 +193,13 @@ sealed class TrustedDeviceEvent {
|
||||||
val email: String,
|
val email: String,
|
||||||
) : TrustedDeviceEvent()
|
) : TrustedDeviceEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the lock screen.
|
||||||
|
*/
|
||||||
|
data class NavigateToLockScreen(
|
||||||
|
val email: String,
|
||||||
|
) : TrustedDeviceEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the [message] as a toast.
|
* Displays the [message] as a toast.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,11 +1,33 @@
|
||||||
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
|
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType
|
||||||
|
|
||||||
const val VAULT_UNLOCK_ROUTE: String = "vault_unlock"
|
private const val VAULT_UNLOCK_TYPE: String = "unlock_type"
|
||||||
|
private const val TDE_VAULT_UNLOCK_ROUTE_PREFIX: String = "tde_vault_unlock"
|
||||||
|
private const val TDE_VAULT_UNLOCK_ROUTE: String =
|
||||||
|
"$TDE_VAULT_UNLOCK_ROUTE_PREFIX/{$VAULT_UNLOCK_TYPE}"
|
||||||
|
private const val VAULT_UNLOCK_ROUTE_PREFIX: String = "vault_unlock"
|
||||||
|
const val VAULT_UNLOCK_ROUTE: String = "$VAULT_UNLOCK_ROUTE_PREFIX/{$VAULT_UNLOCK_TYPE}"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to retrieve vault unlock arguments from the [SavedStateHandle].
|
||||||
|
*/
|
||||||
|
@OmitFromCoverage
|
||||||
|
data class VaultUnlockArgs(
|
||||||
|
val unlockType: UnlockType,
|
||||||
|
) {
|
||||||
|
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||||
|
unlockType = checkNotNull(savedStateHandle.get<UnlockType>(VAULT_UNLOCK_TYPE)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to the Vault Unlock screen.
|
* Navigate to the Vault Unlock screen.
|
||||||
|
@ -13,7 +35,10 @@ const val VAULT_UNLOCK_ROUTE: String = "vault_unlock"
|
||||||
fun NavController.navigateToVaultUnlock(
|
fun NavController.navigateToVaultUnlock(
|
||||||
navOptions: NavOptions? = null,
|
navOptions: NavOptions? = null,
|
||||||
) {
|
) {
|
||||||
navigate(VAULT_UNLOCK_ROUTE, navOptions)
|
navigate(
|
||||||
|
route = "$VAULT_UNLOCK_ROUTE_PREFIX/${UnlockType.STANDARD}",
|
||||||
|
navOptions = navOptions,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,6 +47,35 @@ fun NavController.navigateToVaultUnlock(
|
||||||
fun NavGraphBuilder.vaultUnlockDestination() {
|
fun NavGraphBuilder.vaultUnlockDestination() {
|
||||||
composable(
|
composable(
|
||||||
route = VAULT_UNLOCK_ROUTE,
|
route = VAULT_UNLOCK_ROUTE,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument(VAULT_UNLOCK_TYPE) { type = NavType.EnumType(UnlockType::class.java) },
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
VaultUnlockScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the Vault Unlock screen for TDE.
|
||||||
|
*/
|
||||||
|
fun NavController.navigateToTdeVaultUnlock(
|
||||||
|
navOptions: NavOptions? = null,
|
||||||
|
) {
|
||||||
|
navigate(
|
||||||
|
route = "$TDE_VAULT_UNLOCK_ROUTE_PREFIX/${UnlockType.TDE}",
|
||||||
|
navOptions = navOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the Vault Unlock screen to the TDE nav graph.
|
||||||
|
*/
|
||||||
|
fun NavGraphBuilder.tdeVaultUnlockDestination() {
|
||||||
|
composable(
|
||||||
|
route = TDE_VAULT_UNLOCK_ROUTE,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument(VAULT_UNLOCK_TYPE) { type = NavType.EnumType(UnlockType::class.java) },
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
VaultUnlockScreen()
|
VaultUnlockScreen()
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,11 +138,13 @@ fun VaultUnlockScreen(
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
navigationIcon = null,
|
navigationIcon = null,
|
||||||
actions = {
|
actions = {
|
||||||
BitwardenAccountActionItem(
|
if (state.showAccountMenu) {
|
||||||
initials = state.initials,
|
BitwardenAccountActionItem(
|
||||||
color = state.avatarColor,
|
initials = state.initials,
|
||||||
onClick = { accountMenuVisible = !accountMenuVisible },
|
color = state.avatarColor,
|
||||||
)
|
onClick = { accountMenuVisible = !accountMenuVisible },
|
||||||
|
)
|
||||||
|
}
|
||||||
BitwardenOverflowActionItem(
|
BitwardenOverflowActionItem(
|
||||||
menuItemDataList = persistentListOf(
|
menuItemDataList = persistentListOf(
|
||||||
OverflowMenuItemData(
|
OverflowMenuItemData(
|
||||||
|
@ -162,29 +164,33 @@ fun VaultUnlockScreen(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
BitwardenPasswordField(
|
if (!state.hideInput) {
|
||||||
label = state.vaultUnlockType.unlockScreenInputLabel(),
|
BitwardenPasswordField(
|
||||||
value = state.input,
|
label = state.vaultUnlockType.unlockScreenInputLabel(),
|
||||||
onValueChange = remember(viewModel) {
|
value = state.input,
|
||||||
{ viewModel.trySendAction(VaultUnlockAction.InputChanged(it)) }
|
onValueChange = remember(viewModel) {
|
||||||
},
|
{ viewModel.trySendAction(VaultUnlockAction.InputChanged(it)) }
|
||||||
keyboardType = state.vaultUnlockType.unlockScreenKeyboardType,
|
},
|
||||||
showPasswordTestTag = state.vaultUnlockType.inputFieldVisibilityToggleTestTag,
|
keyboardType = state.vaultUnlockType.unlockScreenKeyboardType,
|
||||||
modifier = Modifier
|
showPasswordTestTag = state
|
||||||
.semantics { testTag = state.vaultUnlockType.unlockScreenInputTestTag }
|
.vaultUnlockType
|
||||||
.padding(horizontal = 16.dp)
|
.inputFieldVisibilityToggleTestTag,
|
||||||
.fillMaxWidth(),
|
modifier = Modifier
|
||||||
)
|
.semantics { testTag = state.vaultUnlockType.unlockScreenInputTestTag }
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
.padding(horizontal = 16.dp)
|
||||||
Text(
|
.fillMaxWidth(),
|
||||||
text = state.vaultUnlockType.unlockScreenMessage(),
|
)
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
Text(
|
||||||
modifier = Modifier
|
text = state.vaultUnlockType.unlockScreenMessage(),
|
||||||
.padding(horizontal = 16.dp)
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
.fillMaxWidth(),
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
modifier = Modifier
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
text = stringResource(
|
||||||
id = R.string.logged_in_as_on,
|
id = R.string.logged_in_as_on,
|
||||||
|
@ -220,17 +226,19 @@ fun VaultUnlockScreen(
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
BitwardenFilledButton(
|
if (!state.hideInput) {
|
||||||
label = stringResource(id = R.string.unlock),
|
BitwardenFilledButton(
|
||||||
onClick = remember(viewModel) {
|
label = stringResource(id = R.string.unlock),
|
||||||
{ viewModel.trySendAction(VaultUnlockAction.UnlockClick) }
|
onClick = remember(viewModel) {
|
||||||
},
|
{ viewModel.trySendAction(VaultUnlockAction.UnlockClick) }
|
||||||
isEnabled = state.input.isNotEmpty(),
|
},
|
||||||
modifier = Modifier
|
isEnabled = state.input.isNotEmpty(),
|
||||||
.semantics { testTag = "UnlockVaultButton" }
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.semantics { testTag = "UnlockVaultButton" }
|
||||||
.fillMaxWidth(),
|
.padding(horizontal = 16.dp)
|
||||||
)
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenErrorMessage
|
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenErrorMessage
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
@ -45,22 +46,33 @@ class VaultUnlockViewModel @Inject constructor(
|
||||||
) : BaseViewModel<VaultUnlockState, VaultUnlockEvent, VaultUnlockAction>(
|
) : BaseViewModel<VaultUnlockState, VaultUnlockEvent, VaultUnlockAction>(
|
||||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||||
|
val trustedDevice = userState.activeAccount.trustedDevice
|
||||||
val accountSummaries = userState.toAccountSummaries()
|
val accountSummaries = userState.toAccountSummaries()
|
||||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||||
val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(
|
val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(
|
||||||
userId = userState.activeUserId,
|
userId = userState.activeUserId,
|
||||||
)
|
)
|
||||||
|
val vaultUnlockType = userState.activeAccount.vaultUnlockType
|
||||||
|
val hasNoMasterPassword = trustedDevice?.hasMasterPassword == false
|
||||||
|
val hideInput = hasNoMasterPassword && vaultUnlockType == VaultUnlockType.MASTER_PASSWORD
|
||||||
|
val isBiometricsEnabled = userState.activeAccount.isBiometricsEnabled
|
||||||
|
if (hasNoMasterPassword && vaultUnlockType != VaultUnlockType.PIN && !isBiometricsEnabled) {
|
||||||
|
// There is no valid way to unlock this app.
|
||||||
|
authRepository.logout()
|
||||||
|
}
|
||||||
VaultUnlockState(
|
VaultUnlockState(
|
||||||
accountSummaries = accountSummaries,
|
accountSummaries = accountSummaries,
|
||||||
avatarColorString = activeAccountSummary.avatarColorHex,
|
avatarColorString = activeAccountSummary.avatarColorHex,
|
||||||
|
hideInput = hideInput,
|
||||||
initials = activeAccountSummary.initials,
|
initials = activeAccountSummary.initials,
|
||||||
email = activeAccountSummary.email,
|
email = activeAccountSummary.email,
|
||||||
dialog = null,
|
dialog = null,
|
||||||
environmentUrl = environmentRepo.environment.label,
|
environmentUrl = environmentRepo.environment.label,
|
||||||
input = "",
|
input = "",
|
||||||
isBiometricEnabled = userState.activeAccount.isBiometricsEnabled,
|
isBiometricEnabled = isBiometricsEnabled,
|
||||||
isBiometricsValid = isBiometricsValid,
|
isBiometricsValid = isBiometricsValid,
|
||||||
vaultUnlockType = userState.activeAccount.vaultUnlockType,
|
showAccountMenu = VaultUnlockArgs(savedStateHandle).unlockType == UnlockType.STANDARD,
|
||||||
|
vaultUnlockType = vaultUnlockType,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
@ -272,6 +284,7 @@ class VaultUnlockViewModel @Inject constructor(
|
||||||
data class VaultUnlockState(
|
data class VaultUnlockState(
|
||||||
val accountSummaries: List<AccountSummary>,
|
val accountSummaries: List<AccountSummary>,
|
||||||
private val avatarColorString: String,
|
private val avatarColorString: String,
|
||||||
|
val hideInput: Boolean,
|
||||||
val initials: String,
|
val initials: String,
|
||||||
val email: String,
|
val email: String,
|
||||||
val environmentUrl: String,
|
val environmentUrl: String,
|
||||||
|
@ -279,6 +292,7 @@ data class VaultUnlockState(
|
||||||
val input: String,
|
val input: String,
|
||||||
val isBiometricsValid: Boolean,
|
val isBiometricsValid: Boolean,
|
||||||
val isBiometricEnabled: Boolean,
|
val isBiometricEnabled: Boolean,
|
||||||
|
val showAccountMenu: Boolean,
|
||||||
val vaultUnlockType: VaultUnlockType,
|
val vaultUnlockType: VaultUnlockType,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VaultUnlockScreen
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the different ways you may want to display the [VaultUnlockScreen].
|
||||||
|
*/
|
||||||
|
enum class UnlockType {
|
||||||
|
STANDARD,
|
||||||
|
TDE,
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ class TrustedDeviceScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
private var onNavigateToAdminApprovalEmail: String? = null
|
private var onNavigateToAdminApprovalEmail: String? = null
|
||||||
private var onNavigateToLoginWithOtherDeviceEmail: String? = null
|
private var onNavigateToLoginWithOtherDeviceEmail: String? = null
|
||||||
|
private var onNavigateToLockEmail: String? = null
|
||||||
|
|
||||||
private val mutableEventFlow = bufferedMutableSharedFlow<TrustedDeviceEvent>()
|
private val mutableEventFlow = bufferedMutableSharedFlow<TrustedDeviceEvent>()
|
||||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
@ -41,6 +42,7 @@ class TrustedDeviceScreenTest : BaseComposeTest() {
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onNavigateToAdminApproval = { onNavigateToAdminApprovalEmail = it },
|
onNavigateToAdminApproval = { onNavigateToAdminApprovalEmail = it },
|
||||||
onNavigateToLoginWithOtherDevice = { onNavigateToLoginWithOtherDeviceEmail = it },
|
onNavigateToLoginWithOtherDevice = { onNavigateToLoginWithOtherDeviceEmail = it },
|
||||||
|
onNavigateToLock = { onNavigateToLockEmail = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,6 +61,13 @@ class TrustedDeviceScreenTest : BaseComposeTest() {
|
||||||
assertEquals(onNavigateToLoginWithOtherDeviceEmail, email)
|
assertEquals(onNavigateToLoginWithOtherDeviceEmail, email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on NavigateToLockScreen event should invoke NavigateToLockScreen`() {
|
||||||
|
val email = "test@bitwarden.com"
|
||||||
|
mutableEventFlow.tryEmit(TrustedDeviceEvent.NavigateToLockScreen(email))
|
||||||
|
assertEquals(onNavigateToLockEmail, email)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on back click should send BackClick`() {
|
fun `on back click should send BackClick`() {
|
||||||
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
||||||
|
|
|
@ -31,6 +31,7 @@ class TrustedDeviceViewModelTest : BaseViewModelTest() {
|
||||||
private val authRepository: AuthRepository = mockk {
|
private val authRepository: AuthRepository = mockk {
|
||||||
every { authStateFlow } returns mutableAuthStateFlow
|
every { authStateFlow } returns mutableAuthStateFlow
|
||||||
every { userStateFlow } returns mutableUserStateFlow
|
every { userStateFlow } returns mutableUserStateFlow
|
||||||
|
every { shouldTrustDevice = any() } just runs
|
||||||
every { logout() } just runs
|
every { logout() } just runs
|
||||||
}
|
}
|
||||||
private val environmentRepo: FakeEnvironmentRepository = FakeEnvironmentRepository()
|
private val environmentRepo: FakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||||
|
@ -196,12 +197,15 @@ class TrustedDeviceViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on ApproveWithPasswordClick emits ShowToast`() = runTest {
|
fun `on ApproveWithPasswordClick emits NavigateToLockScreen`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(TrustedDeviceAction.ApproveWithPasswordClick)
|
viewModel.trySendAction(TrustedDeviceAction.ApproveWithPasswordClick)
|
||||||
assertEquals(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()), awaitItem())
|
assertEquals(TrustedDeviceEvent.NavigateToLockScreen(email = EMAIL), awaitItem())
|
||||||
|
}
|
||||||
|
verify(exactly = 1) {
|
||||||
|
authRepository.shouldTrustDevice = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -382,6 +382,32 @@ class VaultUnlockScreenTest : BaseComposeTest() {
|
||||||
viewModel.trySendAction(VaultUnlockAction.BiometricsLockOut)
|
viewModel.trySendAction(VaultUnlockAction.BiometricsLockOut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `account button should update according to state`() {
|
||||||
|
mutableStateFlow.update { it.copy(showAccountMenu = true) }
|
||||||
|
composeTestRule.onNodeWithText("AU").assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update { it.copy(showAccountMenu = false) }
|
||||||
|
composeTestRule.onNodeWithText("AU").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `input field and unlock button should update according to state`() {
|
||||||
|
mutableStateFlow.update { it.copy(hideInput = false) }
|
||||||
|
composeTestRule.onNodeWithText("Master password").assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Your vault is locked. Verify your master password to continue.")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("Unlock").assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update { it.copy(hideInput = true) }
|
||||||
|
composeTestRule.onNodeWithText("Master password").assertDoesNotExist()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Your vault is locked. Verify your master password to continue.")
|
||||||
|
.assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("Unlock").assertDoesNotExist()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val DEFAULT_ENVIRONMENT_URL: String = "vault.bitwarden.com"
|
private const val DEFAULT_ENVIRONMENT_URL: String = "vault.bitwarden.com"
|
||||||
|
@ -419,9 +445,11 @@ private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
|
||||||
dialog = null,
|
dialog = null,
|
||||||
email = "bit@bitwarden.com",
|
email = "bit@bitwarden.com",
|
||||||
environmentUrl = DEFAULT_ENVIRONMENT_URL,
|
environmentUrl = DEFAULT_ENVIRONMENT_URL,
|
||||||
|
hideInput = false,
|
||||||
initials = "AU",
|
initials = "AU",
|
||||||
input = "",
|
input = "",
|
||||||
isBiometricsValid = true,
|
isBiometricsValid = true,
|
||||||
isBiometricEnabled = true,
|
isBiometricEnabled = true,
|
||||||
|
showAccountMenu = true,
|
||||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentReposito
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
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.components.model.AccountSummary
|
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||||
|
@ -68,6 +69,60 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(state, viewModel.stateFlow.value)
|
assertEquals(state, viewModel.stateFlow.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on init should logout when has no master password, no pin, and no biometrics`() {
|
||||||
|
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
|
||||||
|
accounts = listOf(
|
||||||
|
DEFAULT_ACCOUNT.copy(
|
||||||
|
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||||
|
isBiometricsEnabled = false,
|
||||||
|
trustedDevice = TRUSTED_DEVICE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
createViewModel()
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
authRepository.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on init should not logout when has no master password and no pin, with biometrics`() {
|
||||||
|
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
|
||||||
|
accounts = listOf(
|
||||||
|
DEFAULT_ACCOUNT.copy(
|
||||||
|
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||||
|
isBiometricsEnabled = true,
|
||||||
|
trustedDevice = TRUSTED_DEVICE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
createViewModel()
|
||||||
|
|
||||||
|
verify(exactly = 0) {
|
||||||
|
authRepository.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on init should not logout when has no master password and no biometrics, with pin`() {
|
||||||
|
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
|
||||||
|
accounts = listOf(
|
||||||
|
DEFAULT_ACCOUNT.copy(
|
||||||
|
vaultUnlockType = VaultUnlockType.PIN,
|
||||||
|
isBiometricsEnabled = false,
|
||||||
|
trustedDevice = TRUSTED_DEVICE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
createViewModel()
|
||||||
|
|
||||||
|
verify(exactly = 0) {
|
||||||
|
authRepository.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `environment url should update when environment repo emits an update`() {
|
fun `environment url should update when environment repo emits an update`() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -714,12 +769,16 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createViewModel(
|
private fun createViewModel(
|
||||||
state: VaultUnlockState? = DEFAULT_STATE,
|
state: VaultUnlockState? = null,
|
||||||
|
unlockType: UnlockType = UnlockType.STANDARD,
|
||||||
environmentRepo: EnvironmentRepository = environmentRepository,
|
environmentRepo: EnvironmentRepository = environmentRepository,
|
||||||
vaultRepo: VaultRepository = vaultRepository,
|
vaultRepo: VaultRepository = vaultRepository,
|
||||||
biometricsEncryptionManager: BiometricsEncryptionManager = encryptionManager,
|
biometricsEncryptionManager: BiometricsEncryptionManager = encryptionManager,
|
||||||
): VaultUnlockViewModel = VaultUnlockViewModel(
|
): VaultUnlockViewModel = VaultUnlockViewModel(
|
||||||
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
savedStateHandle = SavedStateHandle().apply {
|
||||||
|
set("state", state)
|
||||||
|
set("unlock_type", unlockType)
|
||||||
|
},
|
||||||
authRepository = authRepository,
|
authRepository = authRepository,
|
||||||
vaultRepo = vaultRepo,
|
vaultRepo = vaultRepo,
|
||||||
environmentRepo = environmentRepo,
|
environmentRepo = environmentRepo,
|
||||||
|
@ -742,15 +801,25 @@ private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
|
||||||
),
|
),
|
||||||
avatarColorString = "#aa00aa",
|
avatarColorString = "#aa00aa",
|
||||||
email = "active@bitwarden.com",
|
email = "active@bitwarden.com",
|
||||||
|
hideInput = false,
|
||||||
initials = "AU",
|
initials = "AU",
|
||||||
dialog = null,
|
dialog = null,
|
||||||
environmentUrl = Environment.Us.label,
|
environmentUrl = Environment.Us.label,
|
||||||
input = "",
|
input = "",
|
||||||
isBiometricsValid = true,
|
isBiometricsValid = true,
|
||||||
isBiometricEnabled = false,
|
isBiometricEnabled = false,
|
||||||
|
showAccountMenu = true,
|
||||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val TRUSTED_DEVICE: UserState.TrustedDevice = UserState.TrustedDevice(
|
||||||
|
isDeviceTrusted = false,
|
||||||
|
hasMasterPassword = false,
|
||||||
|
hasAdminApproval = false,
|
||||||
|
hasLoginApprovingDevice = false,
|
||||||
|
hasResetPasswordPermission = false,
|
||||||
|
)
|
||||||
|
|
||||||
private val DEFAULT_ACCOUNT = UserState.Account(
|
private val DEFAULT_ACCOUNT = UserState.Account(
|
||||||
userId = "activeUserId",
|
userId = "activeUserId",
|
||||||
name = "Active User",
|
name = "Active User",
|
||||||
|
|
|
@ -77,7 +77,7 @@ class RootNavScreenTest : BaseComposeTest() {
|
||||||
rootNavStateFlow.value = RootNavState.VaultLocked
|
rootNavStateFlow.value = RootNavState.VaultLocked
|
||||||
composeTestRule.runOnIdle {
|
composeTestRule.runOnIdle {
|
||||||
fakeNavHostController.assertLastNavigation(
|
fakeNavHostController.assertLastNavigation(
|
||||||
route = "vault_unlock",
|
route = "vault_unlock/STANDARD",
|
||||||
navOptions = expectedNavOptions,
|
navOptions = expectedNavOptions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue