BIT-1508: Implement decline all pending requests & add filters (#845)

This commit is contained in:
Caleb Derosier
2024-01-29 18:48:45 -07:00
committed by Álison Fernandes
parent 88da5b2007
commit f2053bbb07
7 changed files with 415 additions and 123 deletions

View File

@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.auth.repository.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.time.ZonedDateTime
/**
@@ -17,6 +19,7 @@ import java.time.ZonedDateTime
* @param originUrl The origin URL of this auth request.
* @param fingerprint The fingerprint of this auth request.
*/
@Parcelize
data class AuthRequest(
val id: String,
val publicKey: String,
@@ -29,4 +32,4 @@ data class AuthRequest(
val requestApproved: Boolean,
val originUrl: String,
val fingerprint: String,
)
) : Parcelable

View File

@@ -0,0 +1,17 @@
package com.x8bit.bitwarden.ui.platform.base.util
import java.time.Duration
import java.time.ZonedDateTime
import java.util.TimeZone
/**
* Returns a [Boolean] indicating whether this [ZonedDateTime] is five or more minutes old.
*/
@Suppress("MagicNumber")
fun ZonedDateTime.isOverFiveMinutesOld(): Boolean =
Duration
.between(
this,
ZonedDateTime.now(TimeZone.getDefault().toZoneId()),
)
.toMinutes() > 5

View File

@@ -25,7 +25,9 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -44,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButtonWith
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.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
@@ -79,6 +82,7 @@ fun PendingRequestsScreen(
Lifecycle.Event.ON_RESUME -> {
viewModel.trySendAction(PendingRequestsAction.LifecycleResume)
}
else -> Unit
}
}
@@ -107,10 +111,10 @@ fun PendingRequestsScreen(
.padding(innerPadding)
.fillMaxSize(),
state = viewState,
onDeclineAllRequestsClick = remember(viewModel) {
onDeclineAllRequestsConfirm = remember(viewModel) {
{
viewModel.trySendAction(
PendingRequestsAction.DeclineAllRequestsClick,
PendingRequestsAction.DeclineAllRequestsConfirm,
)
}
},
@@ -152,13 +156,31 @@ fun PendingRequestsScreen(
@Composable
private fun PendingRequestsContent(
state: PendingRequestsState.ViewState.Content,
onDeclineAllRequestsClick: () -> Unit,
onDeclineAllRequestsConfirm: () -> Unit,
onNavigateToLoginApproval: (fingerprint: String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
) {
var shouldShowDeclineAllRequestsConfirm by remember { mutableStateOf(false) }
if (shouldShowDeclineAllRequestsConfirm) {
BitwardenTwoButtonDialog(
title = stringResource(R.string.decline_all_requests),
message = stringResource(
id = R.string.are_you_sure_you_want_to_decline_all_pending_log_in_requests,
),
confirmButtonText = stringResource(R.string.yes),
dismissButtonText = stringResource(id = R.string.cancel),
onConfirmClick = {
onDeclineAllRequestsConfirm()
shouldShowDeclineAllRequestsConfirm = false
},
onDismissClick = { shouldShowDeclineAllRequestsConfirm = false },
onDismissRequest = { shouldShowDeclineAllRequestsConfirm = false },
)
}
LazyColumn(
Modifier.padding(bottom = 16.dp),
@@ -181,7 +203,7 @@ private fun PendingRequestsContent(
BitwardenFilledTonalButtonWithIcon(
label = stringResource(id = R.string.decline_all_requests),
icon = painterResource(id = R.drawable.ic_trash),
onClick = onDeclineAllRequestsClick,
onClick = { shouldShowDeclineAllRequestsConfirm = true },
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),

View File

@@ -4,10 +4,11 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequest
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
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 com.x8bit.bitwarden.ui.platform.base.util.isOverFiveMinutesOld
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -27,6 +28,7 @@ class PendingRequestsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<PendingRequestsState, PendingRequestsEvent, PendingRequestsAction>(
initialState = savedStateHandle[KEY_STATE] ?: PendingRequestsState(
authRequests = emptyList(),
viewState = PendingRequestsState.ViewState.Loading,
),
) {
@@ -42,7 +44,7 @@ class PendingRequestsViewModel @Inject constructor(
override fun handleAction(action: PendingRequestsAction) {
when (action) {
PendingRequestsAction.CloseClick -> handleCloseClicked()
PendingRequestsAction.DeclineAllRequestsClick -> handleDeclineAllRequestsClicked()
PendingRequestsAction.DeclineAllRequestsConfirm -> handleDeclineAllRequestsConfirmed()
PendingRequestsAction.LifecycleResume -> handleOnLifecycleResumed()
is PendingRequestsAction.PendingRequestRowClick -> {
handlePendingRequestRowClicked(action)
@@ -58,8 +60,23 @@ class PendingRequestsViewModel @Inject constructor(
sendEvent(PendingRequestsEvent.NavigateBack)
}
private fun handleDeclineAllRequestsClicked() {
sendEvent(PendingRequestsEvent.ShowToast("Not yet implemented.".asText()))
private fun handleDeclineAllRequestsConfirmed() {
viewModelScope.launch {
mutableStateFlow.update {
it.copy(
viewState = PendingRequestsState.ViewState.Loading,
)
}
mutableStateFlow.value.authRequests.forEach { request ->
authRepository.updateAuthRequest(
requestId = request.id,
masterPasswordHash = request.masterPasswordHash,
publicKey = request.publicKey,
isApproved = false,
)
}
updateAuthRequestList()
}
}
private fun handleOnLifecycleResumed() {
@@ -75,30 +92,48 @@ class PendingRequestsViewModel @Inject constructor(
private fun handleAuthRequestsResultReceived(
action: PendingRequestsAction.Internal.AuthRequestsResultReceive,
) {
mutableStateFlow.update {
it.copy(
viewState = when (val result = action.authRequestsResult) {
is AuthRequestsResult.Success -> {
if (result.authRequests.isEmpty()) {
PendingRequestsState.ViewState.Empty
} else {
PendingRequestsState.ViewState.Content(
requests = result.authRequests.map { authRequest ->
PendingRequestsState.ViewState.Content.PendingLoginRequest(
fingerprintPhrase = authRequest.fingerprint,
platform = authRequest.platform,
timestamp = dateTimeFormatter.format(
authRequest.creationDate,
),
)
},
)
}
when (val result = action.authRequestsResult) {
is AuthRequestsResult.Success -> {
val requests = result
.authRequests
.filterRespondedAndExpired()
.sortedByDescending { request -> request.creationDate }
.map { request ->
PendingRequestsState.ViewState.Content.PendingLoginRequest(
fingerprintPhrase = request.fingerprint,
platform = request.platform,
timestamp = dateTimeFormatter.format(
request.creationDate,
),
)
}
if (requests.isEmpty()) {
mutableStateFlow.update {
it.copy(
authRequests = emptyList(),
viewState = PendingRequestsState.ViewState.Empty,
)
}
} else {
mutableStateFlow.update {
it.copy(
authRequests = result.authRequests,
viewState = PendingRequestsState.ViewState.Content(
requests = requests,
),
)
}
}
}
AuthRequestsResult.Error -> PendingRequestsState.ViewState.Error
},
)
AuthRequestsResult.Error -> {
mutableStateFlow.update {
it.copy(
authRequests = emptyList(),
viewState = PendingRequestsState.ViewState.Error,
)
}
}
}
}
@@ -119,6 +154,7 @@ class PendingRequestsViewModel @Inject constructor(
*/
@Parcelize
data class PendingRequestsState(
val authRequests: List<AuthRequest>,
val viewState: ViewState,
) : Parcelable {
/**
@@ -201,9 +237,9 @@ sealed class PendingRequestsAction {
data object CloseClick : PendingRequestsAction()
/**
* The user has clicked to deny all login requests.
* The user has confirmed they want to deny all login requests.
*/
data object DeclineAllRequestsClick : PendingRequestsAction()
data object DeclineAllRequestsConfirm : PendingRequestsAction()
/**
* The screen has been re-opened and should be updated.
@@ -229,3 +265,16 @@ sealed class PendingRequestsAction {
) : Internal()
}
}
/**
* Filters out [AuthRequest]s that match one of the following criteria:
* * The request has been approved.
* * The request has been declined (indicated by it not being approved & having a responseDate).
* * The request has expired (it is at least 5 minutes old).
*/
private fun List<AuthRequest>.filterRespondedAndExpired() =
filterNot { request ->
request.requestApproved ||
request.responseDate != null ||
request.creationDate.isOverFiveMinutesOld()
}