mirror of
https://github.com/bitwarden/android.git
synced 2026-06-08 23:16:33 -05:00
BIT-1565: Approve and decline login requests (#818)
This commit is contained in:
committed by
Álison Fernandes
parent
3be37766e2
commit
a187fbb0d1
@@ -1,10 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /auth-requests API.
|
||||
@@ -19,6 +22,15 @@ interface AuthRequestsApi {
|
||||
@Body body: AuthRequestRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Updates an authentication request.
|
||||
*/
|
||||
@PUT("/auth-requests/{id}")
|
||||
suspend fun updateAuthRequest(
|
||||
@Path("id") userId: String,
|
||||
@Body body: AuthRequestUpdateRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Gets a list of auth requests for this device.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request body for updating an auth request.
|
||||
*/
|
||||
@Serializable
|
||||
data class AuthRequestUpdateRequestJson(
|
||||
@SerialName("key")
|
||||
val key: String,
|
||||
|
||||
@SerialName("masterPasswordHash")
|
||||
val masterPasswordHash: String?,
|
||||
|
||||
@SerialName("deviceIdentifier")
|
||||
val deviceId: String,
|
||||
|
||||
@SerialName("requestApproved")
|
||||
val isApproved: Boolean,
|
||||
)
|
||||
@@ -10,4 +10,15 @@ interface AuthRequestsService {
|
||||
* Gets the list of auth requests for the current user.
|
||||
*/
|
||||
suspend fun getAuthRequests(): Result<AuthRequestsResponseJson>
|
||||
|
||||
/**
|
||||
* Updates an approval request.
|
||||
*/
|
||||
suspend fun updateAuthRequest(
|
||||
requestId: String,
|
||||
key: String,
|
||||
masterPasswordHash: String?,
|
||||
deviceId: String,
|
||||
isApproved: Boolean,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
|
||||
class AuthRequestsServiceImpl(
|
||||
@@ -8,4 +9,21 @@ class AuthRequestsServiceImpl(
|
||||
) : AuthRequestsService {
|
||||
override suspend fun getAuthRequests(): Result<AuthRequestsResponseJson> =
|
||||
authRequestsApi.getAuthRequests()
|
||||
|
||||
override suspend fun updateAuthRequest(
|
||||
requestId: String,
|
||||
key: String,
|
||||
masterPasswordHash: String?,
|
||||
deviceId: String,
|
||||
isApproved: Boolean,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
authRequestsApi.updateAuthRequest(
|
||||
userId = requestId,
|
||||
body = AuthRequestUpdateRequestJson(
|
||||
key = key,
|
||||
masterPasswordHash = masterPasswordHash,
|
||||
deviceId = deviceId,
|
||||
isApproved = isApproved,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -196,6 +196,17 @@ interface AuthRepository : AuthenticatorProvider {
|
||||
*/
|
||||
suspend fun getAuthRequests(): AuthRequestsResult
|
||||
|
||||
/**
|
||||
* Approves or declines the request corresponding to this [requestId] based on [publicKey]
|
||||
* according to [isApproved].
|
||||
*/
|
||||
suspend fun updateAuthRequest(
|
||||
requestId: String,
|
||||
masterPasswordHash: String?,
|
||||
publicKey: String,
|
||||
isApproved: Boolean,
|
||||
): AuthRequestResult
|
||||
|
||||
/**
|
||||
* Get a [Boolean] indicating whether this is a known device.
|
||||
*/
|
||||
|
||||
@@ -59,6 +59,7 @@ import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -76,7 +77,7 @@ import javax.inject.Singleton
|
||||
/**
|
||||
* Default implementation of [AuthRepository].
|
||||
*/
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
|
||||
@Singleton
|
||||
class AuthRepositoryImpl(
|
||||
private val accountsService: AccountsService,
|
||||
@@ -87,6 +88,7 @@ class AuthRepositoryImpl(
|
||||
private val newAuthRequestService: NewAuthRequestService,
|
||||
private val organizationService: OrganizationService,
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
@@ -693,6 +695,51 @@ class AuthRepositoryImpl(
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun updateAuthRequest(
|
||||
requestId: String,
|
||||
masterPasswordHash: String?,
|
||||
publicKey: String,
|
||||
isApproved: Boolean,
|
||||
): AuthRequestResult {
|
||||
val userId = activeUserId ?: return AuthRequestResult.Error
|
||||
return vaultSdkSource
|
||||
.getAuthRequestKey(
|
||||
publicKey = publicKey,
|
||||
userId = userId,
|
||||
)
|
||||
.flatMap {
|
||||
authRequestsService
|
||||
.updateAuthRequest(
|
||||
requestId = requestId,
|
||||
key = it,
|
||||
deviceId = authDiskSource.uniqueAppId,
|
||||
masterPasswordHash = masterPasswordHash,
|
||||
isApproved = isApproved,
|
||||
)
|
||||
}
|
||||
.map { request ->
|
||||
AuthRequestResult.Success(
|
||||
authRequest = AuthRequest(
|
||||
id = request.id,
|
||||
publicKey = request.publicKey,
|
||||
platform = request.platform,
|
||||
ipAddress = request.ipAddress,
|
||||
key = request.key,
|
||||
masterPasswordHash = request.masterPasswordHash,
|
||||
creationDate = request.creationDate,
|
||||
responseDate = request.responseDate,
|
||||
requestApproved = request.requestApproved ?: false,
|
||||
originUrl = request.originUrl,
|
||||
fingerprint = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestResult.Error },
|
||||
onSuccess = { it },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult =
|
||||
devicesService
|
||||
.getIsKnownDevice(
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -41,6 +42,7 @@ object AuthRepositoryModule {
|
||||
newAuthRequestService: NewAuthRequestService,
|
||||
organizationService: OrganizationService,
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
dispatchers: DispatcherManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
@@ -55,6 +57,7 @@ object AuthRepositoryModule {
|
||||
newAuthRequestService = newAuthRequestService,
|
||||
organizationService = organizationService,
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = authDiskSource,
|
||||
haveIBeenPwnedService = haveIBeenPwnedService,
|
||||
dispatcherManager = dispatchers,
|
||||
|
||||
@@ -59,6 +59,14 @@ interface VaultSdkSource {
|
||||
encryptedPin: String,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Gets the key for an auth request that is required to approve or decline it.
|
||||
*/
|
||||
suspend fun getAuthRequestKey(
|
||||
publicKey: String,
|
||||
userId: String,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Gets the user's encryption key, which can be used to later unlock their vault via a call to
|
||||
* [initializeCrypto] with [InitUserCryptoMethod.DecryptedKey].
|
||||
|
||||
@@ -56,6 +56,16 @@ class VaultSdkSourceImpl(
|
||||
.derivePinUserKey(encryptedPin = encryptedPin)
|
||||
}
|
||||
|
||||
override suspend fun getAuthRequestKey(
|
||||
publicKey: String,
|
||||
userId: String,
|
||||
): Result<String> =
|
||||
runCatching {
|
||||
getClient(userId = userId)
|
||||
.auth()
|
||||
.approveAuthRequest(publicKey)
|
||||
}
|
||||
|
||||
override suspend fun getUserEncryptionKey(
|
||||
userId: String,
|
||||
): Result<String> =
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.x8bit.bitwarden.ui.platform.base.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
/**
|
||||
* Creates a side effect to observe lifecycle events.
|
||||
*/
|
||||
@Composable
|
||||
fun LivecycleEventEffect(
|
||||
onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit,
|
||||
) {
|
||||
val eventHandler = rememberUpdatedState(onEvent)
|
||||
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
|
||||
|
||||
DisposableEffect(lifecycleOwner.value) {
|
||||
val lifecycle = lifecycleOwner.value.lifecycle
|
||||
val observer = LifecycleEventObserver { owner, event ->
|
||||
eventHandler.value(owner, event)
|
||||
}
|
||||
|
||||
lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ 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.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||
@@ -61,6 +64,20 @@ fun LoginApprovalScreen(
|
||||
}
|
||||
}
|
||||
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = if (state.shouldShowErrorDialog) {
|
||||
BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
)
|
||||
} else {
|
||||
BasicDialogState.Hidden
|
||||
},
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LoginApprovalAction.ErrorDialogDismiss) }
|
||||
},
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginap
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
@@ -29,6 +30,10 @@ class LoginApprovalViewModel @Inject constructor(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: LoginApprovalState(
|
||||
fingerprint = LoginApprovalArgs(savedStateHandle).fingerprint,
|
||||
masterPasswordHash = null,
|
||||
publicKey = "",
|
||||
requestId = "",
|
||||
shouldShowErrorDialog = false,
|
||||
viewState = LoginApprovalState.ViewState.Loading,
|
||||
),
|
||||
) {
|
||||
@@ -52,16 +57,35 @@ class LoginApprovalViewModel @Inject constructor(
|
||||
LoginApprovalAction.ApproveRequestClick -> handleApproveRequestClicked()
|
||||
LoginApprovalAction.CloseClick -> handleCloseClicked()
|
||||
LoginApprovalAction.DeclineRequestClick -> handleDeclineRequestClicked()
|
||||
LoginApprovalAction.ErrorDialogDismiss -> handleErrorDialogDismissed()
|
||||
|
||||
is LoginApprovalAction.Internal.ApproveRequestResultReceive -> {
|
||||
handleApproveRequestResultReceived(action)
|
||||
}
|
||||
|
||||
is LoginApprovalAction.Internal.AuthRequestResultReceive -> {
|
||||
handleAuthRequestResultReceived(action)
|
||||
}
|
||||
|
||||
is LoginApprovalAction.Internal.DeclineRequestResultReceive -> {
|
||||
handleDeclineRequestResultReceived(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleApproveRequestClicked() {
|
||||
// TODO BIT-1565 implement approve login request
|
||||
sendEvent(LoginApprovalEvent.ShowToast("Not yet implemented".asText()))
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
LoginApprovalAction.Internal.DeclineRequestResultReceive(
|
||||
result = authRepository.updateAuthRequest(
|
||||
requestId = mutableStateFlow.value.requestId,
|
||||
masterPasswordHash = mutableStateFlow.value.masterPasswordHash,
|
||||
publicKey = mutableStateFlow.value.publicKey,
|
||||
isApproved = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClicked() {
|
||||
@@ -69,31 +93,85 @@ class LoginApprovalViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleDeclineRequestClicked() {
|
||||
// TODO BIT-1565 implement decline login request
|
||||
sendEvent(LoginApprovalEvent.ShowToast("Not yet implemented".asText()))
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
LoginApprovalAction.Internal.DeclineRequestResultReceive(
|
||||
result = authRepository.updateAuthRequest(
|
||||
requestId = mutableStateFlow.value.requestId,
|
||||
masterPasswordHash = mutableStateFlow.value.masterPasswordHash,
|
||||
publicKey = mutableStateFlow.value.publicKey,
|
||||
isApproved = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleErrorDialogDismissed() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowErrorDialog = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleApproveRequestResultReceived(
|
||||
action: LoginApprovalAction.Internal.ApproveRequestResultReceive,
|
||||
) {
|
||||
when (action.result) {
|
||||
is AuthRequestResult.Success -> {
|
||||
sendEvent(LoginApprovalEvent.ShowToast(R.string.login_approved.asText()))
|
||||
sendEvent(LoginApprovalEvent.NavigateBack)
|
||||
}
|
||||
|
||||
is AuthRequestResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowErrorDialog = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAuthRequestResultReceived(
|
||||
action: LoginApprovalAction.Internal.AuthRequestResultReceive,
|
||||
) {
|
||||
val email = authRepository.userStateFlow.value?.activeAccount?.email ?: return
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = when (val result = action.authRequestResult) {
|
||||
is AuthRequestResult.Success -> {
|
||||
LoginApprovalState.ViewState.Content(
|
||||
deviceType = result.authRequest.platform,
|
||||
domainUrl = result.authRequest.originUrl,
|
||||
email = email,
|
||||
fingerprint = result.authRequest.fingerprint,
|
||||
ipAddress = result.authRequest.ipAddress,
|
||||
time = dateTimeFormatter.format(result.authRequest.creationDate),
|
||||
)
|
||||
}
|
||||
when (val result = action.authRequestResult) {
|
||||
is AuthRequestResult.Success -> mutableStateFlow.update {
|
||||
it.copy(
|
||||
masterPasswordHash = result.authRequest.masterPasswordHash,
|
||||
publicKey = result.authRequest.publicKey,
|
||||
requestId = result.authRequest.id,
|
||||
viewState = LoginApprovalState.ViewState.Content(
|
||||
deviceType = result.authRequest.platform,
|
||||
domainUrl = result.authRequest.originUrl,
|
||||
email = email,
|
||||
fingerprint = result.authRequest.fingerprint,
|
||||
ipAddress = result.authRequest.ipAddress,
|
||||
time = dateTimeFormatter.format(result.authRequest.creationDate),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is AuthRequestResult.Error -> LoginApprovalState.ViewState.Error
|
||||
},
|
||||
)
|
||||
is AuthRequestResult.Error -> mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginApprovalState.ViewState.Error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeclineRequestResultReceived(
|
||||
action: LoginApprovalAction.Internal.DeclineRequestResultReceive,
|
||||
) {
|
||||
when (action.result) {
|
||||
is AuthRequestResult.Success -> {
|
||||
sendEvent(LoginApprovalEvent.NavigateBack)
|
||||
}
|
||||
|
||||
is AuthRequestResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowErrorDialog = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,8 +181,13 @@ class LoginApprovalViewModel @Inject constructor(
|
||||
*/
|
||||
@Parcelize
|
||||
data class LoginApprovalState(
|
||||
val fingerprint: String,
|
||||
val viewState: ViewState,
|
||||
val shouldShowErrorDialog: Boolean,
|
||||
// Internal
|
||||
val fingerprint: String,
|
||||
val masterPasswordHash: String?,
|
||||
val publicKey: String,
|
||||
val requestId: String,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Represents the specific view states for the [LoginApprovalScreen].
|
||||
@@ -176,15 +259,34 @@ sealed class LoginApprovalAction {
|
||||
*/
|
||||
data object DeclineRequestClick : LoginApprovalAction()
|
||||
|
||||
/**
|
||||
* User dismissed the error dialog.
|
||||
*/
|
||||
data object ErrorDialogDismiss : LoginApprovalAction()
|
||||
|
||||
/**
|
||||
* Models action the view model could send itself.
|
||||
*/
|
||||
sealed class Internal : LoginApprovalAction() {
|
||||
/**
|
||||
* A new result for a request to approve this request has been received.
|
||||
*/
|
||||
data class ApproveRequestResultReceive(
|
||||
val result: AuthRequestResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* An auth request result has been received to populate the data on the screen.
|
||||
*/
|
||||
data class AuthRequestResultReceive(
|
||||
val authRequestResult: AuthRequestResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* A new result for a request to decline this request has been received.
|
||||
*/
|
||||
data class DeclineRequestResultReceive(
|
||||
val result: AuthRequestResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ 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 androidx.lifecycle.Lifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButtonWithIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||
@@ -72,6 +74,15 @@ fun PendingRequestsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LivecycleEventEffect { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
viewModel.trySendAction(PendingRequestsAction.LifecycleResume)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -23,7 +23,7 @@ private const val KEY_STATE = "state"
|
||||
*/
|
||||
@HiltViewModel
|
||||
class PendingRequestsViewModel @Inject constructor(
|
||||
authRepository: AuthRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<PendingRequestsState, PendingRequestsEvent, PendingRequestsAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: PendingRequestsState(
|
||||
@@ -36,19 +36,14 @@ class PendingRequestsViewModel @Inject constructor(
|
||||
.withZone(TimeZone.getDefault().toZoneId())
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
PendingRequestsAction.Internal.AuthRequestsResultReceive(
|
||||
authRequestsResult = authRepository.getAuthRequests(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateAuthRequestList()
|
||||
}
|
||||
|
||||
override fun handleAction(action: PendingRequestsAction) {
|
||||
when (action) {
|
||||
PendingRequestsAction.CloseClick -> handleCloseClicked()
|
||||
PendingRequestsAction.DeclineAllRequestsClick -> handleDeclineAllRequestsClicked()
|
||||
PendingRequestsAction.LifecycleResume -> handleOnLifecycleResumed()
|
||||
is PendingRequestsAction.PendingRequestRowClick -> {
|
||||
handlePendingRequestRowClicked(action)
|
||||
}
|
||||
@@ -67,6 +62,10 @@ class PendingRequestsViewModel @Inject constructor(
|
||||
sendEvent(PendingRequestsEvent.ShowToast("Not yet implemented.".asText()))
|
||||
}
|
||||
|
||||
private fun handleOnLifecycleResumed() {
|
||||
updateAuthRequestList()
|
||||
}
|
||||
|
||||
private fun handlePendingRequestRowClicked(
|
||||
action: PendingRequestsAction.PendingRequestRowClick,
|
||||
) {
|
||||
@@ -102,6 +101,17 @@ class PendingRequestsViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAuthRequestList() {
|
||||
// TODO BIT-1574: Display pull to refresh
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
PendingRequestsAction.Internal.AuthRequestsResultReceive(
|
||||
authRequestsResult = authRepository.getAuthRequests(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,6 +205,11 @@ sealed class PendingRequestsAction {
|
||||
*/
|
||||
data object DeclineAllRequestsClick : PendingRequestsAction()
|
||||
|
||||
/**
|
||||
* The screen has been re-opened and should be updated.
|
||||
*/
|
||||
data object LifecycleResume : PendingRequestsAction()
|
||||
|
||||
/**
|
||||
* The user has clicked one of the pending request rows.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user