BIT-2295: Simplify the pending requests UI (#1325)

This commit is contained in:
David Perez authored and Álison Fernandes committed 2024-06-20 17:08:07 +01:00
1 parent b739be712a
commit 25c7ed0835
17 files changed
+77 -673

No files matched your search

@@ -221,19 +221,6 @@ interface SettingsDiskSource {
blockedAutofillUris: List<String>?,
)
/**
* Gets whether or not the given [userId] has enabled approving passwordless logins.
*/
fun getApprovePasswordlessLoginsEnabled(userId: String): Boolean?
/**
* Stores whether or not [isApprovePasswordlessLoginsEnabled] for the given [userId].
*/
fun storeApprovePasswordlessLoginsEnabled(
userId: String,
isApprovePasswordlessLoginsEnabled: Boolean?,
)
/**
* Gets whether or not the given [userId] has enabled screen capture.
*/
@@ -26,7 +26,6 @@ private const val DEFAULT_URI_MATCH_TYPE_KEY = "defaultUriMatch"
private const val DISABLE_AUTO_TOTP_COPY_KEY = "disableAutoTotpCopy"
private const val DISABLE_AUTOFILL_SAVE_PROMPT_KEY = "autofillDisableSavePrompt"
private const val DISABLE_ICON_LOADING_KEY = "disableFavicon"
private const val APPROVE_PASSWORDLESS_LOGINS_KEY = "approvePasswordlessLogins"
private const val SCREEN_CAPTURE_ALLOW_KEY = "screenCaptureAllowed"
private const val SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY = "biometricIntegritySource"
private const val ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY = "accountBiometricIntegrityValid"
@@ -139,10 +138,6 @@ class SettingsDiskSourceImpl(
storePullToRefreshEnabled(userId = userId, isPullToRefreshEnabled = null)
storeInlineAutofillEnabled(userId = userId, isInlineAutofillEnabled = null)
storeBlockedAutofillUris(userId = userId, blockedAutofillUris = null)
storeApprovePasswordlessLoginsEnabled(
userId = userId,
isApprovePasswordlessLoginsEnabled = null,
)
storeLastSyncTime(userId = userId, lastSyncTime = null)
storeClearClipboardFrequencySeconds(userId = userId, frequency = null)
removeWithPrefix(prefix = ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY.appendIdentifier(userId))
@@ -356,20 +351,6 @@ class SettingsDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
override fun getApprovePasswordlessLoginsEnabled(userId: String): Boolean? {
return getBoolean(key = APPROVE_PASSWORDLESS_LOGINS_KEY.appendIdentifier(userId))
}
override fun storeApprovePasswordlessLoginsEnabled(
userId: String,
isApprovePasswordlessLoginsEnabled: Boolean?,
) {
putBoolean(
key = APPROVE_PASSWORDLESS_LOGINS_KEY.appendIdentifier(userId),
value = isApprovePasswordlessLoginsEnabled,
)
}
override fun getScreenCaptureAllowed(userId: String): Boolean? {
return getBoolean(key = SCREEN_CAPTURE_ALLOW_KEY.appendIdentifier(userId))
}
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
@@ -36,10 +35,8 @@ import javax.inject.Inject
/**
* Primary implementation of [PushManager].
*/
@Suppress("LongParameterList")
class PushManagerImpl @Inject constructor(
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val pushDiskSource: PushDiskSource,
private val pushService: PushService,
private val clock: Clock,
@@ -125,16 +122,14 @@ class PushManagerImpl @Inject constructor(
NotificationType.AUTH_REQUEST,
NotificationType.AUTH_REQUEST_RESPONSE,
-> {
if (settingsDiskSource.getApprovePasswordlessLoginsEnabled(userId) == true) {
val payload: NotificationPayload.PasswordlessRequestNotification =
json.decodeFromString(string = notification.payload)
mutablePasswordlessRequestSharedFlow.tryEmit(
PasswordlessRequestData(
loginRequestId = payload.id,
userId = payload.userId,
),
)
}
val payload: NotificationPayload.PasswordlessRequestNotification =
json.decodeFromString(string = notification.payload)
mutablePasswordlessRequestSharedFlow.tryEmit(
PasswordlessRequestData(
loginRequestId = payload.id,
userId = payload.userId,
),
)
}
NotificationType.LOG_OUT -> {
@@ -143,7 +143,6 @@ object PlatformManagerModule {
@Singleton
fun providePushManager(
authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
pushDiskSource: PushDiskSource,
pushService: PushService,
dispatcherManager: DispatcherManager,
@@ -151,7 +150,6 @@ object PlatformManagerModule {
json: Json,
): PushManager = PushManagerImpl(
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
pushDiskSource = pushDiskSource,
pushService = pushService,
dispatcherManager = dispatcherManager,
@@ -118,11 +118,6 @@ interface SettingsRepository {
*/
var blockedAutofillUris: List<String>
/**
* Whether or not approving passwordless logins is enabled for the current user.
*/
var isApprovePasswordlessLoginsEnabled: Boolean
/**
* Emits updates whenever there is a change in the app's status for supporting autofill.
*
@@ -245,19 +245,6 @@ class SettingsRepositoryImpl(
)
}
override var isApprovePasswordlessLoginsEnabled: Boolean
get() = activeUserId
?.let {
settingsDiskSource.getApprovePasswordlessLoginsEnabled(it)
}
?: false
set(value) {
val userId = activeUserId ?: return
settingsDiskSource.storeApprovePasswordlessLoginsEnabled(
userId = userId,
isApprovePasswordlessLoginsEnabled = value,
)
}
override val isAutofillEnabledStateFlow: StateFlow<Boolean> =
autofillEnabledManager.isAutofillEnabledStateFlow
@@ -1,7 +1,5 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
import android.Manifest
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -58,10 +56,8 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.composition.LocalPermissionsManager
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
import com.x8bit.bitwarden.ui.platform.util.displayLabel
@@ -84,7 +80,6 @@ fun AccountSecurityScreen(
viewModel: AccountSecurityViewModel = hiltViewModel(),
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
intentManager: IntentManager = LocalIntentManager.current,
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
) {
val state by viewModel.stateFlow.collectAsState()
val context = LocalContext.current
@@ -149,7 +144,7 @@ fun AccountSecurityScreen(
},
) { innerPadding ->
Column(
Modifier
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
@@ -160,31 +155,15 @@ fun AccountSecurityScreen(
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
ApprovePasswordlessLoginsRow(
isApproveLoginRequestsEnabled = state.isApproveLoginRequestsEnabled,
onApprovePasswordlessLoginsAction = remember(viewModel) {
{ viewModel.trySendAction(it) }
},
permissionsManager = permissionsManager,
onPushNotificationConfirm = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.PushNotificationConfirm) }
BitwardenTextRow(
text = stringResource(id = R.string.pending_log_in_requests),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.PendingLoginRequestsClick) }
},
modifier = Modifier
.testTag("ApproveLoginRequestsSwitch")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.testTag("PendingLogInRequestsLabel")
.fillMaxWidth(),
)
if (state.isApproveLoginRequestsEnabled) {
BitwardenTextRow(
text = stringResource(id = R.string.pending_log_in_requests),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.PendingLoginRequestsClick) }
},
modifier = Modifier
.testTag("PendingLogInRequestsLabel")
.fillMaxWidth(),
)
}
Spacer(Modifier.height(16.dp))
BitwardenListHeaderText(
@@ -824,92 +803,3 @@ private fun FingerPrintPhraseDialog(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
)
}
@Suppress("LongMethod")
@Composable
private fun ApprovePasswordlessLoginsRow(
isApproveLoginRequestsEnabled: Boolean,
@Suppress("MaxLineLength")
onApprovePasswordlessLoginsAction: (AccountSecurityAction.ApprovePasswordlessLoginsToggle) -> Unit,
onPushNotificationConfirm: () -> Unit,
permissionsManager: PermissionsManager,
modifier: Modifier = Modifier,
) {
var shouldShowConfirmationDialog by remember { mutableStateOf(false) }
var shouldShowPermissionDialog by remember { mutableStateOf(false) }
BitwardenWideSwitch(
label = stringResource(
id = R.string.use_this_device_to_approve_login_requests_made_from_other_devices,
),
isChecked = isApproveLoginRequestsEnabled,
onCheckedChange = { isChecked ->
if (isChecked) {
onApprovePasswordlessLoginsAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.PendingEnabled,
)
shouldShowConfirmationDialog = true
} else {
onApprovePasswordlessLoginsAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled,
)
}
},
modifier = modifier,
)
if (shouldShowConfirmationDialog) {
BitwardenTwoButtonDialog(
title = stringResource(id = R.string.approve_login_requests),
message = stringResource(
id = R.string.use_this_device_to_approve_login_requests_made_from_other_devices,
),
confirmButtonText = stringResource(id = R.string.yes),
dismissButtonText = stringResource(id = R.string.no),
onConfirmClick = {
onApprovePasswordlessLoginsAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Enabled,
)
shouldShowConfirmationDialog = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@Suppress("MaxLineLength")
if (!permissionsManager.checkPermission(Manifest.permission.POST_NOTIFICATIONS)) {
shouldShowPermissionDialog = true
}
}
},
onDismissClick = {
onApprovePasswordlessLoginsAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled,
)
shouldShowConfirmationDialog = false
},
onDismissRequest = {
onApprovePasswordlessLoginsAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled,
)
shouldShowConfirmationDialog = false
},
)
}
if (shouldShowPermissionDialog) {
BitwardenTwoButtonDialog(
title = null,
message = stringResource(
id = R.string.receive_push_notifications_for_new_login_requests,
),
confirmButtonText = stringResource(id = R.string.settings),
dismissButtonText = stringResource(id = R.string.no_thanks),
onConfirmClick = {
shouldShowPermissionDialog = false
onPushNotificationConfirm()
},
onDismissClick = {
shouldShowPermissionDialog = false
},
onDismissRequest = {
shouldShowPermissionDialog = false
},
)
}
}
@@ -41,14 +41,13 @@ class AccountSecurityViewModel @Inject constructor(
private val vaultRepository: VaultRepository,
private val settingsRepository: SettingsRepository,
private val environmentRepository: EnvironmentRepository,
private val policyManager: PolicyManager,
policyManager: PolicyManager,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<AccountSecurityState, AccountSecurityEvent, AccountSecurityAction>(
initialState = savedStateHandle[KEY_STATE]
?: AccountSecurityState(
dialog = null,
fingerprintPhrase = "".asText(), // This will be filled in dynamically
isApproveLoginRequestsEnabled = settingsRepository.isApprovePasswordlessLoginsEnabled,
isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled,
isUnlockWithPasswordEnabled = authRepository
.userStateFlow
@@ -118,11 +117,6 @@ class AccountSecurityViewModel @Inject constructor(
}
is AccountSecurityAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action)
is AccountSecurityAction.ApprovePasswordlessLoginsToggle -> {
handleApprovePasswordlessLoginsToggle(action)
}
is AccountSecurityAction.PushNotificationConfirm -> handlePushNotificationConfirm()
is AccountSecurityAction.Internal -> handleInternalAction(action)
}
@@ -158,26 +152,6 @@ class AccountSecurityViewModel @Inject constructor(
vaultRepository.lockVaultForCurrentUser()
}
private fun handleApprovePasswordlessLoginsToggle(
action: AccountSecurityAction.ApprovePasswordlessLoginsToggle,
) {
when (action) {
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled -> {
settingsRepository.isApprovePasswordlessLoginsEnabled = false
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = false) }
}
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Enabled -> {
settingsRepository.isApprovePasswordlessLoginsEnabled = true
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = true) }
}
AccountSecurityAction.ApprovePasswordlessLoginsToggle.PendingEnabled -> {
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = true) }
}
}
}
private fun handlePushNotificationConfirm() {
sendEvent(AccountSecurityEvent.NavigateToApplicationDataSettings)
}
@@ -368,7 +342,6 @@ class AccountSecurityViewModel @Inject constructor(
data class AccountSecurityState(
val dialog: AccountSecurityDialog?,
val fingerprintPhrase: Text,
val isApproveLoginRequestsEnabled: Boolean,
val isUnlockWithBiometricsEnabled: Boolean,
val isUnlockWithPasswordEnabled: Boolean,
val isUnlockWithPinEnabled: Boolean,
@@ -551,26 +524,6 @@ sealed class AccountSecurityAction {
*/
data object PushNotificationConfirm : AccountSecurityAction()
/**
* User toggled the approve passwordless logins switch.
*/
sealed class ApprovePasswordlessLoginsToggle : AccountSecurityAction() {
/**
* The toggle was enabled and confirmed.
*/
data object Enabled : ApprovePasswordlessLoginsToggle()
/**
* The toggle was enabled but not yet confirmed.
*/
data object PendingEnabled : ApprovePasswordlessLoginsToggle()
/**
* The toggle was disabled.
*/
data object Disabled : ApprovePasswordlessLoginsToggle()
}
/**
* User toggled the unlock with pin switch.
*/
@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.vault
import android.Manifest
import android.os.Build
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn
@@ -54,9 +56,11 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.composition.LocalPermissionsManager
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
@@ -80,6 +84,7 @@ fun VaultScreen(
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
exitManager: ExitManager = LocalExitManager.current,
intentManager: IntentManager = LocalIntentManager.current,
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
) {
val state by viewModel.stateFlow.collectAsState()
val context = LocalContext.current
@@ -120,6 +125,7 @@ fun VaultScreen(
}
}
val vaultHandlers = remember(viewModel) { VaultHandlers.create(viewModel) }
VaultScreenPushNotifications(permissionsManager = permissionsManager)
VaultScreenScaffold(
state = state,
pullToRefreshState = pullToRefreshState,
@@ -128,6 +134,25 @@ fun VaultScreen(
)
}
/**
* Handles the notifications permission request.
*/
@Composable
private fun VaultScreenPushNotifications(
permissionsManager: PermissionsManager,
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
val launcher = permissionsManager.getLauncher {
// We do not actually care what the response is, we just need
// to give the user a chance to give us the permission.
}
LaunchedEffect(key1 = Unit) {
if (!permissionsManager.checkPermission(Manifest.permission.POST_NOTIFICATIONS)) {
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}
/**
* Scaffold for the [VaultScreen]
*/