mirror of
https://github.com/bitwarden/android.git
synced 2026-06-06 06:17:21 -05:00
BIT-1508: Implement decline all pending requests & add filters (#845)
This commit is contained in:
committed by
Álison Fernandes
parent
88da5b2007
commit
f2053bbb07
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user