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,
|
||||
onNavigateToDeleteAccount: () -> Unit,
|
||||
onNavigateToFolders: () -> Unit,
|
||||
onNavigateToPendingRequests: () -> Unit,
|
||||
) {
|
||||
navigation(
|
||||
startDestination = SETTINGS_ROUTE,
|
||||
|
@ -51,6 +52,7 @@ fun NavGraphBuilder.settingsGraph(
|
|||
accountSecurityDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
||||
onNavigateToPendingRequests = onNavigateToPendingRequests,
|
||||
)
|
||||
appearanceDestination(onNavigateBack = { navController.popBackStack() })
|
||||
autoFillDestination(
|
||||
|
|
|
@ -13,6 +13,7 @@ private const val ACCOUNT_SECURITY_ROUTE = "settings_account_security"
|
|||
fun NavGraphBuilder.accountSecurityDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToDeleteAccount: () -> Unit,
|
||||
onNavigateToPendingRequests: () -> Unit,
|
||||
) {
|
||||
composableWithPushTransitions(
|
||||
route = ACCOUNT_SECURITY_ROUTE,
|
||||
|
@ -20,6 +21,7 @@ fun NavGraphBuilder.accountSecurityDestination(
|
|||
AccountSecurityScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
||||
onNavigateToPendingRequests = onNavigateToPendingRequests,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ private const val MINUTES_PER_HOUR = 60
|
|||
fun AccountSecurityScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToDeleteAccount: () -> Unit,
|
||||
onNavigateToPendingRequests: () -> Unit,
|
||||
viewModel: AccountSecurityViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
||||
|
@ -91,6 +92,8 @@ fun AccountSecurityScreen(
|
|||
intentManager.launchUri("http://bitwarden.com/help/fingerprint-phrase".toUri())
|
||||
}
|
||||
|
||||
AccountSecurityEvent.NavigateToPendingRequests -> onNavigateToPendingRequests()
|
||||
|
||||
is AccountSecurityEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.text(resources), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
|
|
@ -144,8 +144,7 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handlePendingLoginRequestsClick() {
|
||||
// TODO BIT-466: Implement pending login requests UI
|
||||
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
|
||||
sendEvent(AccountSecurityEvent.NavigateToPendingRequests)
|
||||
}
|
||||
|
||||
private fun handleVaultTimeoutTypeSelect(action: AccountSecurityAction.VaultTimeoutTypeSelect) {
|
||||
|
@ -296,6 +295,11 @@ sealed class AccountSecurityEvent {
|
|||
*/
|
||||
data object NavigateToFingerprintPhrase : AccountSecurityEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the Pending Login Requests screen.
|
||||
*/
|
||||
data object NavigateToPendingRequests : AccountSecurityEvent()
|
||||
|
||||
/**
|
||||
* 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.settings.accountsecurity.deleteaccount.deleteAccountDestination
|
||||
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.navigateToFolders
|
||||
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) },
|
||||
onNavigateToEditSend = { navController.navigateToAddSend(AddSendType.EditItem(it)) },
|
||||
onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() },
|
||||
onNavigateToPendingRequests = { navController.navigateToPendingRequests() },
|
||||
onNavigateToPasswordHistory = { navController.navigateToPasswordHistory() },
|
||||
)
|
||||
deleteAccountDestination(onNavigateBack = { navController.popBackStack() })
|
||||
pendingRequestsDestination(onNavigateBack = { navController.popBackStack() })
|
||||
vaultAddEditDestination(
|
||||
onNavigateToQrCodeScanScreen = {
|
||||
navController.navigateToQrCodeScanScreen()
|
||||
|
|
|
@ -32,6 +32,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
|||
onNavigateToEditSend: (sendItemId: String) -> Unit,
|
||||
onNavigateToDeleteAccount: () -> Unit,
|
||||
onNavigateToFolders: () -> Unit,
|
||||
onNavigateToPendingRequests: () -> Unit,
|
||||
onNavigateToPasswordHistory: () -> Unit,
|
||||
) {
|
||||
composableWithStayTransitions(
|
||||
|
@ -47,6 +48,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
|||
onNavigateToEditSend = onNavigateToEditSend,
|
||||
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
|
||||
onNavigateToFolders = onNavigateToFolders,
|
||||
onNavigateToPendingRequests = onNavigateToPendingRequests,
|
||||
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ fun VaultUnlockedNavBarScreen(
|
|||
onNavigateToEditSend: (sendItemId: String) -> Unit,
|
||||
onNavigateToDeleteAccount: () -> Unit,
|
||||
onNavigateToFolders: () -> Unit,
|
||||
onNavigateToPendingRequests: () -> Unit,
|
||||
onNavigateToPasswordHistory: () -> Unit,
|
||||
) {
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
|
@ -125,6 +126,7 @@ fun VaultUnlockedNavBarScreen(
|
|||
onNavigateToEditSend = onNavigateToEditSend,
|
||||
navigateToDeleteAccount = onNavigateToDeleteAccount,
|
||||
navigateToFolders = onNavigateToFolders,
|
||||
navigateToPendingRequests = onNavigateToPendingRequests,
|
||||
navigateToPasswordHistory = onNavigateToPasswordHistory,
|
||||
generatorTabClickedAction = {
|
||||
viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick)
|
||||
|
@ -162,6 +164,7 @@ private fun VaultUnlockedNavBarScaffold(
|
|||
onNavigateToEditSend: (sendItemId: String) -> Unit,
|
||||
navigateToDeleteAccount: () -> Unit,
|
||||
navigateToFolders: () -> Unit,
|
||||
navigateToPendingRequests: () -> Unit,
|
||||
navigateToPasswordHistory: () -> Unit,
|
||||
) {
|
||||
var shouldDimNavBar by remember { mutableStateOf(false) }
|
||||
|
@ -235,6 +238,7 @@ private fun VaultUnlockedNavBarScaffold(
|
|||
navController = navController,
|
||||
onNavigateToDeleteAccount = navigateToDeleteAccount,
|
||||
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 onNavigateToDeleteAccountCalled = false
|
||||
private var onNavigateToPendingRequestsCalled = false
|
||||
|
||||
private val intentManager = mockk<IntentManager> {
|
||||
every { launchUri(any()) } just runs
|
||||
|
@ -61,6 +62,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
AccountSecurityScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToDeleteAccount = { onNavigateToDeleteAccountCalled = true },
|
||||
onNavigateToPendingRequests = { onNavigateToPendingRequestsCalled = true },
|
||||
viewModel = viewModel,
|
||||
intentManager = intentManager,
|
||||
permissionsManager = permissionsManager,
|
||||
|
@ -1100,11 +1102,17 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToDeleteAccount should call onNavigateToDeleteAccountCalled`() {
|
||||
fun `on NavigateToDeleteAccount should call onNavigateToDeleteAccount`() {
|
||||
mutableEventFlow.tryEmit(AccountSecurityEvent.NavigateToDeleteAccount)
|
||||
assertTrue(onNavigateToDeleteAccountCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToPendingRequests should call onNavigateToPendingRequests`() {
|
||||
mutableEventFlow.tryEmit(AccountSecurityEvent.NavigateToPendingRequests)
|
||||
assertTrue(onNavigateToPendingRequestsCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirm dialog be shown or hidden according to the state`() {
|
||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||
|
|
|
@ -115,12 +115,12 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on PendingLoginRequestsClick should emit ShowToast`() = runTest {
|
||||
fun `on PendingLoginRequestsClick should emit NavigateToPendingRequests`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.PendingLoginRequestsClick)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
||||
AccountSecurityEvent.NavigateToPendingRequests,
|
||||
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 = {},
|
||||
onNavigateToFolders = {},
|
||||
onNavigateToPasswordHistory = {},
|
||||
onNavigateToPendingRequests = {},
|
||||
onNavigateToSearchVault = {},
|
||||
onNavigateToSearchSend = {},
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue