mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1508: Add Pending Login Requests screen (#738)
This commit is contained in:
parent
6a66d24dd1
commit
9a371843ee
16 changed files with 631 additions and 5 deletions
|
@ -30,6 +30,7 @@ fun NavGraphBuilder.settingsGraph(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
onNavigateToDeleteAccount: () -> Unit,
|
onNavigateToDeleteAccount: () -> Unit,
|
||||||
onNavigateToFolders: () -> Unit,
|
onNavigateToFolders: () -> Unit,
|
||||||
|
onNavigateToPendingRequests: () -> Unit,
|
||||||
) {
|
) {
|
||||||
navigation(
|
navigation(
|
||||||
startDestination = SETTINGS_ROUTE,
|
startDestination = SETTINGS_ROUTE,
|
||||||
|
@ -51,6 +52,7 @@ fun NavGraphBuilder.settingsGraph(
|
||||||
accountSecurityDestination(
|
accountSecurityDestination(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
||||||
|
onNavigateToPendingRequests = onNavigateToPendingRequests,
|
||||||
)
|
)
|
||||||
appearanceDestination(onNavigateBack = { navController.popBackStack() })
|
appearanceDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
autoFillDestination(
|
autoFillDestination(
|
||||||
|
|
|
@ -13,6 +13,7 @@ private const val ACCOUNT_SECURITY_ROUTE = "settings_account_security"
|
||||||
fun NavGraphBuilder.accountSecurityDestination(
|
fun NavGraphBuilder.accountSecurityDestination(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToDeleteAccount: () -> Unit,
|
onNavigateToDeleteAccount: () -> Unit,
|
||||||
|
onNavigateToPendingRequests: () -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithPushTransitions(
|
composableWithPushTransitions(
|
||||||
route = ACCOUNT_SECURITY_ROUTE,
|
route = ACCOUNT_SECURITY_ROUTE,
|
||||||
|
@ -20,6 +21,7 @@ fun NavGraphBuilder.accountSecurityDestination(
|
||||||
AccountSecurityScreen(
|
AccountSecurityScreen(
|
||||||
onNavigateBack = onNavigateBack,
|
onNavigateBack = onNavigateBack,
|
||||||
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
||||||
|
onNavigateToPendingRequests = onNavigateToPendingRequests,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ private const val MINUTES_PER_HOUR = 60
|
||||||
fun AccountSecurityScreen(
|
fun AccountSecurityScreen(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToDeleteAccount: () -> Unit,
|
onNavigateToDeleteAccount: () -> Unit,
|
||||||
|
onNavigateToPendingRequests: () -> Unit,
|
||||||
viewModel: AccountSecurityViewModel = hiltViewModel(),
|
viewModel: AccountSecurityViewModel = hiltViewModel(),
|
||||||
intentManager: IntentManager = LocalIntentManager.current,
|
intentManager: IntentManager = LocalIntentManager.current,
|
||||||
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
||||||
|
@ -91,6 +92,8 @@ fun AccountSecurityScreen(
|
||||||
intentManager.launchUri("http://bitwarden.com/help/fingerprint-phrase".toUri())
|
intentManager.launchUri("http://bitwarden.com/help/fingerprint-phrase".toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccountSecurityEvent.NavigateToPendingRequests -> onNavigateToPendingRequests()
|
||||||
|
|
||||||
is AccountSecurityEvent.ShowToast -> {
|
is AccountSecurityEvent.ShowToast -> {
|
||||||
Toast.makeText(context, event.text(resources), Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, event.text(resources), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,8 +144,7 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePendingLoginRequestsClick() {
|
private fun handlePendingLoginRequestsClick() {
|
||||||
// TODO BIT-466: Implement pending login requests UI
|
sendEvent(AccountSecurityEvent.NavigateToPendingRequests)
|
||||||
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleVaultTimeoutTypeSelect(action: AccountSecurityAction.VaultTimeoutTypeSelect) {
|
private fun handleVaultTimeoutTypeSelect(action: AccountSecurityAction.VaultTimeoutTypeSelect) {
|
||||||
|
@ -296,6 +295,11 @@ sealed class AccountSecurityEvent {
|
||||||
*/
|
*/
|
||||||
data object NavigateToFingerprintPhrase : AccountSecurityEvent()
|
data object NavigateToFingerprintPhrase : AccountSecurityEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the Pending Login Requests screen.
|
||||||
|
*/
|
||||||
|
data object NavigateToPendingRequests : AccountSecurityEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a toast with the given [Text].
|
* Displays a toast with the given [Text].
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||||
|
|
||||||
|
private const val PENDING_REQUESTS_ROUTE = "pending_requests"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add pending requests destinations to the nav graph.
|
||||||
|
*/
|
||||||
|
fun NavGraphBuilder.pendingRequestsDestination(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
composableWithSlideTransitions(
|
||||||
|
route = PENDING_REQUESTS_ROUTE,
|
||||||
|
) {
|
||||||
|
PendingRequestsScreen(onNavigateBack = onNavigateBack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the Pending Login Requests screen.
|
||||||
|
*/
|
||||||
|
fun NavController.navigateToPendingRequests(navOptions: NavOptions? = null) {
|
||||||
|
navigate(PENDING_REQUESTS_ROUTE, navOptions)
|
||||||
|
}
|
|
@ -0,0 +1,267 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
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.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButtonWithIcon
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the pending login requests screen.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
fun PendingRequestsScreen(
|
||||||
|
viewModel: PendingRequestsViewModel = hiltViewModel(),
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsState()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val resources = context.resources
|
||||||
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
PendingRequestsEvent.NavigateBack -> onNavigateBack()
|
||||||
|
|
||||||
|
is PendingRequestsEvent.ShowToast -> {
|
||||||
|
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
BitwardenScaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
BitwardenTopAppBar(
|
||||||
|
title = stringResource(id = R.string.pending_log_in_requests),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||||
|
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||||
|
onNavigationIconClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(PendingRequestsAction.CloseClick) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
|
when (val viewState = state.viewState) {
|
||||||
|
is PendingRequestsState.ViewState.Content -> {
|
||||||
|
PendingRequestsContent(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
state = viewState,
|
||||||
|
onDeclineAllRequestsClick = remember(viewModel) {
|
||||||
|
{
|
||||||
|
viewModel.trySendAction(
|
||||||
|
PendingRequestsAction.DeclineAllRequestsClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PendingRequestsState.ViewState.Empty -> PendingRequestsEmpty(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
)
|
||||||
|
|
||||||
|
is PendingRequestsState.ViewState.Error -> BitwardenErrorContent(
|
||||||
|
message = viewState.message.toString(resources),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
)
|
||||||
|
|
||||||
|
is PendingRequestsState.ViewState.Loading -> BitwardenLoadingContent(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models the list content for the Pending Requests screen.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun PendingRequestsContent(
|
||||||
|
state: PendingRequestsState.ViewState.Content,
|
||||||
|
onDeclineAllRequestsClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
Modifier.padding(bottom = 16.dp),
|
||||||
|
) {
|
||||||
|
items(state.requests) { request ->
|
||||||
|
PendingRequestItem(
|
||||||
|
fingerprintPhrase = request.fingerprintPhrase,
|
||||||
|
platform = request.platform,
|
||||||
|
timestamp = request.timestamp,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BitwardenFilledTonalButtonWithIcon(
|
||||||
|
label = stringResource(id = R.string.decline_all_requests),
|
||||||
|
icon = painterResource(id = R.drawable.ic_trash),
|
||||||
|
onClick = onDeclineAllRequestsClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a pending request item to display in the list.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun PendingRequestItem(
|
||||||
|
fingerprintPhrase: String,
|
||||||
|
platform: String,
|
||||||
|
timestamp: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.fingerprint_phrase),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = fingerprintPhrase,
|
||||||
|
color = LocalNonMaterialColors.current.fingerprint,
|
||||||
|
style = LocalNonMaterialTypography.current.sensitiveInfoSmall,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = platform,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = timestamp,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models the empty state for the Pending Requests screen.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun PendingRequestsEmpty(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.ic_pending_requests),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.no_pending_requests),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
|
Spacer(modifier = Modifier.height(64.dp))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
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.asText
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val KEY_STATE = "state"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View model for the pending login requests screen.
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
class PendingRequestsViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
) : BaseViewModel<PendingRequestsState, PendingRequestsEvent, PendingRequestsAction>(
|
||||||
|
initialState = savedStateHandle[KEY_STATE] ?: PendingRequestsState(
|
||||||
|
viewState = PendingRequestsState.ViewState.Empty,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
// TODO BIT-1291: make /auth-requests call
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: PendingRequestsAction) {
|
||||||
|
when (action) {
|
||||||
|
PendingRequestsAction.CloseClick -> handleCloseClicked()
|
||||||
|
PendingRequestsAction.DeclineAllRequestsClick -> handleDeclineAllRequestsClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCloseClicked() {
|
||||||
|
sendEvent(PendingRequestsEvent.NavigateBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDeclineAllRequestsClicked() {
|
||||||
|
sendEvent(PendingRequestsEvent.ShowToast("Not yet implemented.".asText()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models state for the Pending Login Requests screen.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class PendingRequestsState(
|
||||||
|
val viewState: ViewState,
|
||||||
|
) : Parcelable {
|
||||||
|
/**
|
||||||
|
* Represents the specific view states for the [PendingRequestsScreen].
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
sealed class ViewState : Parcelable {
|
||||||
|
/**
|
||||||
|
* Content state for the [PendingRequestsScreen] listing pending request items.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Content(
|
||||||
|
val requests: List<PendingLoginRequest>,
|
||||||
|
) : ViewState() {
|
||||||
|
/**
|
||||||
|
* Models the data for a pending login request.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class PendingLoginRequest(
|
||||||
|
val fingerprintPhrase: String,
|
||||||
|
val platform: String,
|
||||||
|
val timestamp: String,
|
||||||
|
) : Parcelable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state wherein there are no pending login requests.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object Empty : ViewState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a state where the [PendingRequestsScreen] is unable to display data due to an
|
||||||
|
* error retrieving it.
|
||||||
|
*
|
||||||
|
* @property message The message to display on the error screen.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Error(
|
||||||
|
val message: Text,
|
||||||
|
) : ViewState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading state for the [PendingRequestsScreen], signifying that the content is being
|
||||||
|
* processed.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object Loading : ViewState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models events for the delete account screen.
|
||||||
|
*/
|
||||||
|
sealed class PendingRequestsEvent {
|
||||||
|
/**
|
||||||
|
* Navigates back.
|
||||||
|
*/
|
||||||
|
data object NavigateBack : PendingRequestsEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the [message] in a toast.
|
||||||
|
*/
|
||||||
|
data class ShowToast(
|
||||||
|
val message: Text,
|
||||||
|
) : PendingRequestsEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models actions for the delete account screen.
|
||||||
|
*/
|
||||||
|
sealed class PendingRequestsAction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has clicked the close button.
|
||||||
|
*/
|
||||||
|
data object CloseClick : PendingRequestsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has clicked to deny all login requests.
|
||||||
|
*/
|
||||||
|
data object DeclineAllRequestsClick : PendingRequestsAction()
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ import com.x8bit.bitwarden.ui.platform.feature.search.navigateToSearch
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.search.searchDestination
|
import com.x8bit.bitwarden.ui.platform.feature.search.searchDestination
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.deleteAccountDestination
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.deleteAccountDestination
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.navigateToDeleteAccount
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.navigateToDeleteAccount
|
||||||
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests.navigateToPendingRequests
|
||||||
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests.pendingRequestsDestination
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.foldersDestination
|
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.foldersDestination
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.navigateToFolders
|
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.navigateToFolders
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
|
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
|
||||||
|
@ -74,9 +76,11 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
onNavigateToAddSend = { navController.navigateToAddSend(AddSendType.AddItem) },
|
onNavigateToAddSend = { navController.navigateToAddSend(AddSendType.AddItem) },
|
||||||
onNavigateToEditSend = { navController.navigateToAddSend(AddSendType.EditItem(it)) },
|
onNavigateToEditSend = { navController.navigateToAddSend(AddSendType.EditItem(it)) },
|
||||||
onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() },
|
onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() },
|
||||||
|
onNavigateToPendingRequests = { navController.navigateToPendingRequests() },
|
||||||
onNavigateToPasswordHistory = { navController.navigateToPasswordHistory() },
|
onNavigateToPasswordHistory = { navController.navigateToPasswordHistory() },
|
||||||
)
|
)
|
||||||
deleteAccountDestination(onNavigateBack = { navController.popBackStack() })
|
deleteAccountDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
|
pendingRequestsDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
vaultAddEditDestination(
|
vaultAddEditDestination(
|
||||||
onNavigateToQrCodeScanScreen = {
|
onNavigateToQrCodeScanScreen = {
|
||||||
navController.navigateToQrCodeScanScreen()
|
navController.navigateToQrCodeScanScreen()
|
||||||
|
|
|
@ -32,6 +32,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
||||||
onNavigateToEditSend: (sendItemId: String) -> Unit,
|
onNavigateToEditSend: (sendItemId: String) -> Unit,
|
||||||
onNavigateToDeleteAccount: () -> Unit,
|
onNavigateToDeleteAccount: () -> Unit,
|
||||||
onNavigateToFolders: () -> Unit,
|
onNavigateToFolders: () -> Unit,
|
||||||
|
onNavigateToPendingRequests: () -> Unit,
|
||||||
onNavigateToPasswordHistory: () -> Unit,
|
onNavigateToPasswordHistory: () -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithStayTransitions(
|
composableWithStayTransitions(
|
||||||
|
@ -47,6 +48,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
||||||
onNavigateToEditSend = onNavigateToEditSend,
|
onNavigateToEditSend = onNavigateToEditSend,
|
||||||
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
||||||
onNavigateToFolders = onNavigateToFolders,
|
onNavigateToFolders = onNavigateToFolders,
|
||||||
|
onNavigateToPendingRequests = onNavigateToPendingRequests,
|
||||||
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
|
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,7 @@ fun VaultUnlockedNavBarScreen(
|
||||||
onNavigateToEditSend: (sendItemId: String) -> Unit,
|
onNavigateToEditSend: (sendItemId: String) -> Unit,
|
||||||
onNavigateToDeleteAccount: () -> Unit,
|
onNavigateToDeleteAccount: () -> Unit,
|
||||||
onNavigateToFolders: () -> Unit,
|
onNavigateToFolders: () -> Unit,
|
||||||
|
onNavigateToPendingRequests: () -> Unit,
|
||||||
onNavigateToPasswordHistory: () -> Unit,
|
onNavigateToPasswordHistory: () -> Unit,
|
||||||
) {
|
) {
|
||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
|
@ -125,6 +126,7 @@ fun VaultUnlockedNavBarScreen(
|
||||||
onNavigateToEditSend = onNavigateToEditSend,
|
onNavigateToEditSend = onNavigateToEditSend,
|
||||||
navigateToDeleteAccount = onNavigateToDeleteAccount,
|
navigateToDeleteAccount = onNavigateToDeleteAccount,
|
||||||
navigateToFolders = onNavigateToFolders,
|
navigateToFolders = onNavigateToFolders,
|
||||||
|
navigateToPendingRequests = onNavigateToPendingRequests,
|
||||||
navigateToPasswordHistory = onNavigateToPasswordHistory,
|
navigateToPasswordHistory = onNavigateToPasswordHistory,
|
||||||
generatorTabClickedAction = {
|
generatorTabClickedAction = {
|
||||||
viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick)
|
viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick)
|
||||||
|
@ -162,6 +164,7 @@ private fun VaultUnlockedNavBarScaffold(
|
||||||
onNavigateToEditSend: (sendItemId: String) -> Unit,
|
onNavigateToEditSend: (sendItemId: String) -> Unit,
|
||||||
navigateToDeleteAccount: () -> Unit,
|
navigateToDeleteAccount: () -> Unit,
|
||||||
navigateToFolders: () -> Unit,
|
navigateToFolders: () -> Unit,
|
||||||
|
navigateToPendingRequests: () -> Unit,
|
||||||
navigateToPasswordHistory: () -> Unit,
|
navigateToPasswordHistory: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var shouldDimNavBar by remember { mutableStateOf(false) }
|
var shouldDimNavBar by remember { mutableStateOf(false) }
|
||||||
|
@ -235,6 +238,7 @@ private fun VaultUnlockedNavBarScaffold(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onNavigateToDeleteAccount = navigateToDeleteAccount,
|
onNavigateToDeleteAccount = navigateToDeleteAccount,
|
||||||
onNavigateToFolders = navigateToFolders,
|
onNavigateToFolders = navigateToFolders,
|
||||||
|
onNavigateToPendingRequests = navigateToPendingRequests,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
33
app/src/main/res/drawable/ic_pending_requests.xml
Normal file
33
app/src/main/res/drawable/ic_pending_requests.xml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="198dp"
|
||||||
|
android:height="118dp"
|
||||||
|
android:viewportHeight="118"
|
||||||
|
android:viewportWidth="198">
|
||||||
|
<path
|
||||||
|
android:fillColor="#8E8E93"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M33.94,31.88H9.98C8.39,31.88 6.87,32.51 5.74,33.63C4.62,34.76 3.99,36.28 3.99,37.87V97.77C3.99,99.36 4.62,100.88 5.74,102.01C6.87,103.13 8.39,103.76 9.98,103.76H33.94C35.53,103.76 37.05,103.13 38.18,102.01C39.3,100.88 39.93,99.36 39.93,97.77V37.87C39.93,36.28 39.3,34.76 38.18,33.63C37.05,32.51 35.53,31.88 33.94,31.88ZM9.98,27.89C7.33,27.89 4.79,28.94 2.92,30.81C1.05,32.68 -0,35.22 -0,37.87V97.77C-0,100.42 1.05,102.96 2.92,104.83C4.79,106.7 7.33,107.75 9.98,107.75H33.94C36.59,107.75 39.13,106.7 41,104.83C42.87,102.96 43.92,100.42 43.92,97.77V37.87C43.92,35.22 42.87,32.68 41,30.81C39.13,28.94 36.59,27.89 33.94,27.89H9.98Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#8E8E93"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M20.33,36.67C20.33,36.41 20.44,36.15 20.63,35.97C20.81,35.78 21.07,35.67 21.33,35.67H22.57C22.83,35.67 23.09,35.78 23.28,35.97C23.46,36.15 23.57,36.41 23.57,36.67C23.57,36.94 23.46,37.19 23.28,37.38C23.09,37.57 22.83,37.67 22.57,37.67H21.33C21.07,37.67 20.81,37.57 20.63,37.38C20.44,37.19 20.33,36.94 20.33,36.67ZM67.88,108.75C67.88,108.49 67.99,108.23 68.18,108.05C68.36,107.86 68.62,107.75 68.88,107.75H126.79C127.05,107.75 127.31,107.86 127.49,108.05C127.68,108.23 127.79,108.49 127.79,108.75C127.79,109.02 127.68,109.27 127.49,109.46C127.31,109.65 127.05,109.75 126.79,109.75H68.88C68.62,109.75 68.36,109.65 68.18,109.46C67.99,109.27 67.88,109.02 67.88,108.75Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#8E8E93"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M87.59,108.75V91.02H89.59V108.75H87.59ZM107.53,108.75V91.02H109.53V108.75H107.53Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#8E8E93"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M26.95,13.91C26.95,10.2 28.42,6.65 31.05,4.03C33.67,1.41 37.22,-0.07 40.93,-0.07H156.74C160.45,-0.07 164,1.41 166.62,4.03C169.24,6.65 170.71,10.2 170.71,13.91V19.9H166.72V13.91C166.72,11.26 165.67,8.72 163.8,6.85C161.93,4.98 159.39,3.93 156.74,3.93H40.93C38.28,3.93 35.74,4.98 33.87,6.85C32,8.72 30.95,11.26 30.95,13.91V29.88H26.95V13.91ZM41.93,87.79H126.79V91.78H41.93V87.79Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#8E8E93"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M34.94,14.91C34.94,13.05 35.68,11.28 36.99,9.97C38.3,8.66 40.07,7.92 41.93,7.92H155.74C157.59,7.92 159.37,8.66 160.68,9.97C161.99,11.28 162.73,13.05 162.73,14.91V19.9H160.73V14.91C160.73,13.58 160.21,12.31 159.27,11.38C158.33,10.44 157.06,9.92 155.74,9.92H41.93C40.6,9.92 39.33,10.44 38.4,11.38C37.46,12.31 36.94,13.58 36.94,14.91V29.88H34.94V14.91ZM41.93,81.8H126.79V83.79H41.93V81.8Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#8E8E93"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M124.79,27.89C124.79,25.24 125.84,22.7 127.71,20.83C129.59,18.95 132.13,17.9 134.77,17.9H187.69C190.34,17.9 192.87,18.95 194.75,20.83C196.62,22.7 197.67,25.24 197.67,27.89V107.75C197.67,110.4 196.62,112.94 194.75,114.81C192.87,116.69 190.34,117.74 187.69,117.74H134.77C132.13,117.74 129.59,116.69 127.71,114.81C125.84,112.94 124.79,110.4 124.79,107.75V27.89ZM134.77,21.9C133.19,21.9 131.66,22.53 130.54,23.65C129.41,24.77 128.79,26.3 128.79,27.89V107.75C128.79,109.34 129.41,110.87 130.54,111.99C131.66,113.11 133.19,113.75 134.77,113.75H187.69C189.28,113.75 190.8,113.11 191.92,111.99C193.05,110.87 193.68,109.34 193.68,107.75V27.89C193.68,26.3 193.05,24.77 191.92,23.65C190.8,22.53 189.28,21.9 187.69,21.9H134.77Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#8E8E93"
|
||||||
|
android:pathData="M163.73,108.75C163.73,109.28 163.52,109.79 163.14,110.17C162.77,110.54 162.26,110.75 161.73,110.75C161.2,110.75 160.69,110.54 160.32,110.17C159.94,109.79 159.73,109.28 159.73,108.75C159.73,108.22 159.94,107.72 160.32,107.34C160.69,106.97 161.2,106.76 161.73,106.76C162.26,106.76 162.77,106.97 163.14,107.34C163.52,107.72 163.73,108.22 163.73,108.75Z" />
|
||||||
|
</vector>
|
|
@ -41,6 +41,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
private var onNavigateBackCalled = false
|
private var onNavigateBackCalled = false
|
||||||
private var onNavigateToDeleteAccountCalled = false
|
private var onNavigateToDeleteAccountCalled = false
|
||||||
|
private var onNavigateToPendingRequestsCalled = false
|
||||||
|
|
||||||
private val intentManager = mockk<IntentManager> {
|
private val intentManager = mockk<IntentManager> {
|
||||||
every { launchUri(any()) } just runs
|
every { launchUri(any()) } just runs
|
||||||
|
@ -61,6 +62,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
||||||
AccountSecurityScreen(
|
AccountSecurityScreen(
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
onNavigateToDeleteAccount = { onNavigateToDeleteAccountCalled = true },
|
onNavigateToDeleteAccount = { onNavigateToDeleteAccountCalled = true },
|
||||||
|
onNavigateToPendingRequests = { onNavigateToPendingRequestsCalled = true },
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
intentManager = intentManager,
|
intentManager = intentManager,
|
||||||
permissionsManager = permissionsManager,
|
permissionsManager = permissionsManager,
|
||||||
|
@ -1100,11 +1102,17 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on NavigateToDeleteAccount should call onNavigateToDeleteAccountCalled`() {
|
fun `on NavigateToDeleteAccount should call onNavigateToDeleteAccount`() {
|
||||||
mutableEventFlow.tryEmit(AccountSecurityEvent.NavigateToDeleteAccount)
|
mutableEventFlow.tryEmit(AccountSecurityEvent.NavigateToDeleteAccount)
|
||||||
assertTrue(onNavigateToDeleteAccountCalled)
|
assertTrue(onNavigateToDeleteAccountCalled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on NavigateToPendingRequests should call onNavigateToPendingRequests`() {
|
||||||
|
mutableEventFlow.tryEmit(AccountSecurityEvent.NavigateToPendingRequests)
|
||||||
|
assertTrue(onNavigateToPendingRequestsCalled)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `confirm dialog be shown or hidden according to the state`() {
|
fun `confirm dialog be shown or hidden according to the state`() {
|
||||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||||
|
|
|
@ -115,12 +115,12 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on PendingLoginRequestsClick should emit ShowToast`() = runTest {
|
fun `on PendingLoginRequestsClick should emit NavigateToPendingRequests`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(AccountSecurityAction.PendingLoginRequestsClick)
|
viewModel.trySendAction(AccountSecurityAction.PendingLoginRequestsClick)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
AccountSecurityEvent.NavigateToPendingRequests,
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
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 kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
|
||||||
|
class PendingRequestsScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
|
private var onNavigateBackCalled = false
|
||||||
|
|
||||||
|
private val mutableEventFlow = bufferedMutableSharedFlow<PendingRequestsEvent>()
|
||||||
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
private val viewModel = mockk<PendingRequestsViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns mutableEventFlow
|
||||||
|
every { stateFlow } returns mutableStateFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
PendingRequestsScreen(
|
||||||
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on NavigateBack should call onNavigateBack`() {
|
||||||
|
mutableEventFlow.tryEmit(PendingRequestsEvent.NavigateBack)
|
||||||
|
assertTrue(onNavigateBackCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on DeclineAllRequestsClick should send DeclineAllRequestsClick`() = runTest {
|
||||||
|
// set content so the Decline all requests button appears
|
||||||
|
mutableStateFlow.tryEmit(
|
||||||
|
PendingRequestsState(
|
||||||
|
viewState = PendingRequestsState.ViewState.Content(
|
||||||
|
requests = listOf(
|
||||||
|
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||||
|
fingerprintPhrase = "pantry-overdue-survive-sleep-jab",
|
||||||
|
platform = "iOS",
|
||||||
|
timestamp = "8/24/2023 11:11 AM",
|
||||||
|
),
|
||||||
|
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||||
|
fingerprintPhrase = "erupt-anew-matchbook-disk-student",
|
||||||
|
platform = "Android",
|
||||||
|
timestamp = "8/21/2023 9:43 AM",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule.onNodeWithText("Decline all requests").performClick()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(PendingRequestsAction.DeclineAllRequestsClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
|
||||||
|
viewState = PendingRequestsState.ViewState.Empty,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class PendingRequestsViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct when not set`() {
|
||||||
|
val viewModel = createViewModel(state = null)
|
||||||
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct when set`() {
|
||||||
|
val state = DEFAULT_STATE.copy(
|
||||||
|
viewState = PendingRequestsState.ViewState.Loading,
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel(state = state)
|
||||||
|
assertEquals(state, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on CloseClick should emit NavigateBack`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(PendingRequestsAction.CloseClick)
|
||||||
|
assertEquals(PendingRequestsEvent.NavigateBack, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on DeclineAllRequestsClick should send ShowToast event`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(PendingRequestsAction.DeclineAllRequestsClick)
|
||||||
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
|
assertEquals(
|
||||||
|
PendingRequestsEvent.ShowToast("Not yet implemented.".asText()),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewModel(
|
||||||
|
state: PendingRequestsState? = DEFAULT_STATE,
|
||||||
|
): PendingRequestsViewModel = PendingRequestsViewModel(
|
||||||
|
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
|
||||||
|
viewState = PendingRequestsState.ViewState.Empty,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,6 +47,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||||
onNavigateToDeleteAccount = {},
|
onNavigateToDeleteAccount = {},
|
||||||
onNavigateToFolders = {},
|
onNavigateToFolders = {},
|
||||||
onNavigateToPasswordHistory = {},
|
onNavigateToPasswordHistory = {},
|
||||||
|
onNavigateToPendingRequests = {},
|
||||||
onNavigateToSearchVault = {},
|
onNavigateToSearchVault = {},
|
||||||
onNavigateToSearchSend = {},
|
onNavigateToSearchSend = {},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue