mirror of
https://github.com/bitwarden/android.git
synced 2024-11-27 03:49:36 +03:00
BIT-72: Adding UI and navigation for the master password hint screen (#720)
This commit is contained in:
parent
fa0b71df75
commit
427299eddf
11 changed files with 444 additions and 4 deletions
|
@ -18,6 +18,8 @@ import com.x8bit.bitwarden.ui.auth.feature.login.loginDestination
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.login.navigateToLogin
|
import com.x8bit.bitwarden.ui.auth.feature.login.navigateToLogin
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.loginWithDeviceDestination
|
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.loginWithDeviceDestination
|
||||||
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.masterpasswordhint.masterPasswordHintDestination
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.navigateToMasterPasswordHint
|
||||||
|
|
||||||
const val AUTH_GRAPH_ROUTE: String = "auth_graph"
|
const val AUTH_GRAPH_ROUTE: String = "auth_graph"
|
||||||
|
|
||||||
|
@ -58,6 +60,11 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
||||||
)
|
)
|
||||||
loginDestination(
|
loginDestination(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
onNavigateToMasterPasswordHint = { emailAddress ->
|
||||||
|
navController.navigateToMasterPasswordHint(
|
||||||
|
emailAddress = emailAddress,
|
||||||
|
)
|
||||||
|
},
|
||||||
onNavigateToEnterpriseSignOn = { navController.navigateToEnterpriseSignOn() },
|
onNavigateToEnterpriseSignOn = { navController.navigateToEnterpriseSignOn() },
|
||||||
onNavigateToLoginWithDevice = { navController.navigateToLoginWithDevice() },
|
onNavigateToLoginWithDevice = { navController.navigateToLoginWithDevice() },
|
||||||
)
|
)
|
||||||
|
@ -67,6 +74,9 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
||||||
environmentDestination(
|
environmentDestination(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
|
masterPasswordHintDestination(
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ fun NavController.navigateToLogin(
|
||||||
*/
|
*/
|
||||||
fun NavGraphBuilder.loginDestination(
|
fun NavGraphBuilder.loginDestination(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
|
onNavigateToMasterPasswordHint: (emailAddress: String) -> Unit,
|
||||||
onNavigateToEnterpriseSignOn: () -> Unit,
|
onNavigateToEnterpriseSignOn: () -> Unit,
|
||||||
onNavigateToLoginWithDevice: () -> Unit,
|
onNavigateToLoginWithDevice: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
@ -58,6 +59,7 @@ fun NavGraphBuilder.loginDestination(
|
||||||
) {
|
) {
|
||||||
LoginScreen(
|
LoginScreen(
|
||||||
onNavigateBack = onNavigateBack,
|
onNavigateBack = onNavigateBack,
|
||||||
|
onNavigateToMasterPasswordHint = onNavigateToMasterPasswordHint,
|
||||||
onNavigateToEnterpriseSignOn = onNavigateToEnterpriseSignOn,
|
onNavigateToEnterpriseSignOn = onNavigateToEnterpriseSignOn,
|
||||||
onNavigateToLoginWithDevice = onNavigateToLoginWithDevice,
|
onNavigateToLoginWithDevice = onNavigateToLoginWithDevice,
|
||||||
)
|
)
|
||||||
|
|
|
@ -64,6 +64,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
|
onNavigateToMasterPasswordHint: (String) -> Unit,
|
||||||
onNavigateToEnterpriseSignOn: () -> Unit,
|
onNavigateToEnterpriseSignOn: () -> Unit,
|
||||||
onNavigateToLoginWithDevice: () -> Unit,
|
onNavigateToLoginWithDevice: () -> Unit,
|
||||||
viewModel: LoginViewModel = hiltViewModel(),
|
viewModel: LoginViewModel = hiltViewModel(),
|
||||||
|
@ -74,6 +75,9 @@ fun LoginScreen(
|
||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
LoginEvent.NavigateBack -> onNavigateBack()
|
LoginEvent.NavigateBack -> onNavigateBack()
|
||||||
|
is LoginEvent.NavigateToMasterPasswordHint -> {
|
||||||
|
onNavigateToMasterPasswordHint(event.emailAddress)
|
||||||
|
}
|
||||||
is LoginEvent.NavigateToCaptcha -> {
|
is LoginEvent.NavigateToCaptcha -> {
|
||||||
intentManager.startCustomTabsActivity(uri = event.uri)
|
intentManager.startCustomTabsActivity(uri = event.uri)
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,8 +238,8 @@ class LoginViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMasterPasswordHintClicked() {
|
private fun handleMasterPasswordHintClicked() {
|
||||||
// TODO: Navigate to master password hint screen (BIT-72)
|
val email = mutableStateFlow.value.emailAddress
|
||||||
sendEvent(LoginEvent.ShowToast("Not yet implemented."))
|
sendEvent(LoginEvent.NavigateToMasterPasswordHint(email))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNotYouButtonClicked() {
|
private fun handleNotYouButtonClicked() {
|
||||||
|
@ -285,6 +285,13 @@ sealed class LoginEvent {
|
||||||
*/
|
*/
|
||||||
data object NavigateBack : LoginEvent()
|
data object NavigateBack : LoginEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the master password hit screen.
|
||||||
|
*/
|
||||||
|
data class NavigateToMasterPasswordHint(
|
||||||
|
val emailAddress: String,
|
||||||
|
) : LoginEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to the captcha verification screen.
|
* Navigates to the captcha verification screen.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||||
|
|
||||||
|
private const val EMAIL_ADDRESS: String = "email_address"
|
||||||
|
private const val MASTER_PASSWORD_HINT_ROUTE: String = "master_password_hint/{$EMAIL_ADDRESS}"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to retrieve login arguments from the [SavedStateHandle].
|
||||||
|
*/
|
||||||
|
@OmitFromCoverage
|
||||||
|
data class MasterPasswordHintArgs(val emailAddress: String) {
|
||||||
|
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||||
|
checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the master password hint screen.
|
||||||
|
*/
|
||||||
|
fun NavController.navigateToMasterPasswordHint(
|
||||||
|
emailAddress: String,
|
||||||
|
navOptions: NavOptions? = null,
|
||||||
|
) {
|
||||||
|
this.navigate("master_password_hint/$emailAddress", navOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the master password hint screen to the nav graph.
|
||||||
|
*/
|
||||||
|
fun NavGraphBuilder.masterPasswordHintDestination(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
composableWithSlideTransitions(
|
||||||
|
route = MASTER_PASSWORD_HINT_ROUTE,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
MasterPasswordHintScreen(onNavigateBack = onNavigateBack)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top level composable for the Login screen.
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MasterPasswordHintScreen(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
viewModel: MasterPasswordHintViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
MasterPasswordHintEvent.NavigateBack -> onNavigateBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val dialogState = state.dialog) {
|
||||||
|
is MasterPasswordHintState.DialogState.PasswordHintSent -> {
|
||||||
|
BitwardenBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
title = R.string.password_hint.asText(),
|
||||||
|
message = R.string.password_hint_alert.asText(),
|
||||||
|
),
|
||||||
|
onDismissRequest = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(MasterPasswordHintAction.DismissDialog) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MasterPasswordHintState.DialogState.Error -> {
|
||||||
|
BitwardenBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
title = R.string.password_hint.asText(),
|
||||||
|
message = dialogState.message,
|
||||||
|
),
|
||||||
|
onDismissRequest = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(MasterPasswordHintAction.DismissDialog) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
BitwardenScaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
BitwardenTopAppBar(
|
||||||
|
title = stringResource(id = R.string.password_hint),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||||
|
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||||
|
onNavigationIconClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(MasterPasswordHintAction.CloseClick) }
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
BitwardenTextButton(
|
||||||
|
label = stringResource(id = R.string.submit),
|
||||||
|
onClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(MasterPasswordHintAction.SubmitClick) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
BitwardenTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
value = state.emailInput,
|
||||||
|
onValueChange = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(MasterPasswordHintAction.EmailInputChange(it)) }
|
||||||
|
},
|
||||||
|
label = stringResource(id = R.string.email_address),
|
||||||
|
keyboardType = KeyboardType.Email,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.enter_email_for_hint),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
vertical = 4.dp,
|
||||||
|
horizontal = 16.dp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val KEY_STATE = "state"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View model for the master password hint screen.
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
class MasterPasswordHintViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
) : BaseViewModel<MasterPasswordHintState, MasterPasswordHintEvent, MasterPasswordHintAction>(
|
||||||
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
|
?: MasterPasswordHintState(
|
||||||
|
emailInput = MasterPasswordHintArgs(savedStateHandle).emailAddress,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
stateFlow
|
||||||
|
.onEach {
|
||||||
|
savedStateHandle[KEY_STATE] = it
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: MasterPasswordHintAction) {
|
||||||
|
when (action) {
|
||||||
|
MasterPasswordHintAction.CloseClick -> handleCloseClick()
|
||||||
|
MasterPasswordHintAction.SubmitClick -> handleSubmitClick()
|
||||||
|
is MasterPasswordHintAction.EmailInputChange -> handleEmailInputUpdated(action)
|
||||||
|
MasterPasswordHintAction.DismissDialog -> handleDismissDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCloseClick() {
|
||||||
|
sendEvent(
|
||||||
|
event = MasterPasswordHintEvent.NavigateBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSubmitClick() {
|
||||||
|
// TODO (BIT-71): Implement master password hint
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEmailInputUpdated(action: MasterPasswordHintAction.EmailInputChange) {
|
||||||
|
val email = action.input
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
emailInput = email,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDismissDialog() {
|
||||||
|
mutableStateFlow.update { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models state of the landing screen.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class MasterPasswordHintState(
|
||||||
|
val dialog: DialogState? = null,
|
||||||
|
val emailInput: String,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the current state of any dialogs on screen.
|
||||||
|
*/
|
||||||
|
sealed class DialogState : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a dialog indicating that the password hint was sent.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object PasswordHintSent : DialogState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error dialog with the given [message].
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Error(
|
||||||
|
val message: Text,
|
||||||
|
) : DialogState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models events for the master password hint screen.
|
||||||
|
*/
|
||||||
|
sealed class MasterPasswordHintEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates back to the previous screen.
|
||||||
|
*/
|
||||||
|
data object NavigateBack : MasterPasswordHintEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models actions for the login screen.
|
||||||
|
*/
|
||||||
|
sealed class MasterPasswordHintAction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the top-bar close button was clicked.
|
||||||
|
*/
|
||||||
|
data object CloseClick : MasterPasswordHintAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the top-bar submit button was clicked.
|
||||||
|
*/
|
||||||
|
data object SubmitClick : MasterPasswordHintAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the input on the email field has changed.
|
||||||
|
*/
|
||||||
|
data class EmailInputChange(val input: String) : MasterPasswordHintAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User dismissed the currently displayed dialog.
|
||||||
|
*/
|
||||||
|
data object DismissDialog : MasterPasswordHintAction()
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ class LoginScreenTest : BaseComposeTest() {
|
||||||
every { startCustomTabsActivity(any()) } returns Unit
|
every { startCustomTabsActivity(any()) } returns Unit
|
||||||
}
|
}
|
||||||
private var onNavigateBackCalled = false
|
private var onNavigateBackCalled = false
|
||||||
|
private var onNavigateToMasterPasswordHintCalled = false
|
||||||
private var onNavigateToEnterpriseSignOnCalled = false
|
private var onNavigateToEnterpriseSignOnCalled = false
|
||||||
private var onNavigateToLoginWithDeviceCalled = false
|
private var onNavigateToLoginWithDeviceCalled = false
|
||||||
private val mutableEventFlow = bufferedMutableSharedFlow<LoginEvent>()
|
private val mutableEventFlow = bufferedMutableSharedFlow<LoginEvent>()
|
||||||
|
@ -59,6 +60,7 @@ class LoginScreenTest : BaseComposeTest() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
LoginScreen(
|
LoginScreen(
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
|
onNavigateToMasterPasswordHint = { onNavigateToMasterPasswordHintCalled = true },
|
||||||
onNavigateToEnterpriseSignOn = { onNavigateToEnterpriseSignOnCalled = true },
|
onNavigateToEnterpriseSignOn = { onNavigateToEnterpriseSignOnCalled = true },
|
||||||
onNavigateToLoginWithDevice = { onNavigateToLoginWithDeviceCalled = true },
|
onNavigateToLoginWithDevice = { onNavigateToLoginWithDeviceCalled = true },
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
|
@ -279,6 +281,12 @@ class LoginScreenTest : BaseComposeTest() {
|
||||||
verify { intentManager.startCustomTabsActivity(mockUri) }
|
verify { intentManager.startCustomTabsActivity(mockUri) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateToMasterPasswordHint should call onNavigateToMasterPasswordHint`() {
|
||||||
|
mutableEventFlow.tryEmit(LoginEvent.NavigateToMasterPasswordHint("email"))
|
||||||
|
assertTrue(onNavigateToMasterPasswordHintCalled)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `NavigateToEnterpriseSignOn should call onNavigateToEnterpriseSignOn`() {
|
fun `NavigateToEnterpriseSignOn should call onNavigateToEnterpriseSignOn`() {
|
||||||
mutableEventFlow.tryEmit(LoginEvent.NavigateToEnterpriseSignOn)
|
mutableEventFlow.tryEmit(LoginEvent.NavigateToEnterpriseSignOn)
|
||||||
|
|
|
@ -336,13 +336,13 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `MasterPasswordHintClick should emit ShowToast`() = runTest {
|
fun `MasterPasswordHintClick should emit NavigateToMasterPasswordHint`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.actionChannel.trySend(LoginAction.MasterPasswordHintClick)
|
viewModel.actionChannel.trySend(LoginAction.MasterPasswordHintClick)
|
||||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
LoginEvent.ShowToast("Not yet implemented."),
|
LoginEvent.NavigateToMasterPasswordHint("test@gmail.com"),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performTextReplacement
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MasterPasswordHintScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
|
private var onNavigateBackCalled = false
|
||||||
|
private val mutableEventFlow = bufferedMutableSharedFlow<MasterPasswordHintEvent>()
|
||||||
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
private val viewModel = mockk<MasterPasswordHintViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns mutableEventFlow
|
||||||
|
every { stateFlow } returns mutableStateFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
MasterPasswordHintScreen(
|
||||||
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `email input change should send EmailInputChange action`() {
|
||||||
|
val emailInput = "newEmail"
|
||||||
|
composeTestRule.onNodeWithText("currentEmail").performTextReplacement(emailInput)
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(MasterPasswordHintAction.EmailInputChange(emailInput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateBack should call onNavigateBack`() {
|
||||||
|
mutableEventFlow.tryEmit(MasterPasswordHintEvent.NavigateBack)
|
||||||
|
assertTrue(onNavigateBackCalled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE =
|
||||||
|
MasterPasswordHintState(
|
||||||
|
emailInput = "currentEmail",
|
||||||
|
)
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class MasterPasswordHintViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(DEFAULT_STATE, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on CloseClick should emit NavigateBack`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(MasterPasswordHintAction.CloseClick)
|
||||||
|
assertEquals(MasterPasswordHintEvent.NavigateBack, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewModel(
|
||||||
|
state: MasterPasswordHintState? = DEFAULT_STATE,
|
||||||
|
): MasterPasswordHintViewModel = MasterPasswordHintViewModel(
|
||||||
|
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE: MasterPasswordHintState = MasterPasswordHintState(
|
||||||
|
emailInput = "email",
|
||||||
|
)
|
Loading…
Reference in a new issue