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

This commit is contained in:
David Perez 2024-05-01 10:59:05 -05:00 committed by Álison Fernandes
parent b739be712a
commit 25c7ed0835
17 changed files with 77 additions and 673 deletions

View file

@ -14,6 +14,7 @@
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:name=".BitwardenApplication"

View file

@ -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.
*/

View file

@ -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))
}

View file

@ -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 -> {

View file

@ -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,

View file

@ -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.
*

View file

@ -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

View file

@ -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
},
)
}
}

View file

@ -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.
*/

View file

@ -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]
*/

View file

@ -127,10 +127,6 @@ class SettingsDiskSourceTest {
userId = userId,
blockedAutofillUris = listOf("www.example.com"),
)
settingsDiskSource.storeApprovePasswordlessLoginsEnabled(
userId = userId,
isApprovePasswordlessLoginsEnabled = true,
)
settingsDiskSource.storeLastSyncTime(
userId = userId,
lastSyncTime = Instant.parse("2023-10-27T12:00:00Z"),
@ -161,7 +157,6 @@ class SettingsDiskSourceTest {
assertNull(settingsDiskSource.getPullToRefreshEnabled(userId = userId))
assertNull(settingsDiskSource.getInlineAutofillEnabled(userId = userId))
assertNull(settingsDiskSource.getBlockedAutofillUris(userId = userId))
assertNull(settingsDiskSource.getApprovePasswordlessLoginsEnabled(userId = userId))
assertNull(settingsDiskSource.getLastSyncTime(userId = userId))
assertNull(settingsDiskSource.getClearClipboardFrequencySeconds(userId = userId))
assertNull(
@ -821,67 +816,6 @@ class SettingsDiskSourceTest {
)
}
@Suppress("MaxLineLength")
@Test
fun `getApprovePasswordlessLoginsEnabled when values are present should pull from SharedPreferences`() {
val approvePasswordlessLoginsBaseKey = "bwPreferencesStorage:approvePasswordlessLogins"
val mockUserId = "mockUserId"
val isEnabled = true
fakeSharedPreferences
.edit {
putBoolean(
"${approvePasswordlessLoginsBaseKey}_$mockUserId",
isEnabled,
)
}
val actual = settingsDiskSource.getApprovePasswordlessLoginsEnabled(userId = mockUserId)
assertEquals(
isEnabled,
actual,
)
}
@Test
fun `getApprovePasswordlessLoginsEnabled when values are absent should return null`() {
val mockUserId = "mockUserId"
assertNull(settingsDiskSource.getApprovePasswordlessLoginsEnabled(userId = mockUserId))
}
@Suppress("MaxLineLength")
@Test
fun `storeApprovePasswordlessLoginsEnabled for non-null values should update SharedPreferences`() {
val approvePasswordlessLoginsBaseKey = "bwPreferencesStorage:approvePasswordlessLogins"
val mockUserId = "mockUserId"
val isEnabled = true
settingsDiskSource.storeApprovePasswordlessLoginsEnabled(
userId = mockUserId,
isApprovePasswordlessLoginsEnabled = isEnabled,
)
val actual = fakeSharedPreferences.getBoolean(
"${approvePasswordlessLoginsBaseKey}_$mockUserId",
false,
)
assertEquals(
isEnabled,
actual,
)
}
@Test
fun `storeApprovePasswordlessLoginsEnabled for null values should clear SharedPreferences`() {
val approvePasswordlessLoginsBaseKey = "bwPreferencesStorage:approvePasswordlessLogins"
val mockUserId = "mockUserId"
val approvePasswordlessLoginsKey = "${approvePasswordlessLoginsBaseKey}_$mockUserId"
fakeSharedPreferences.edit {
putBoolean(approvePasswordlessLoginsKey, true)
}
settingsDiskSource.storeApprovePasswordlessLoginsEnabled(
userId = mockUserId,
isApprovePasswordlessLoginsEnabled = null,
)
assertFalse(fakeSharedPreferences.contains(approvePasswordlessLoginsKey))
}
@Test
fun `getScreenCaptureAllowed should pull from SharedPreferences`() {
val screenCaptureAllowBaseKey = "bwPreferencesStorage:screenCaptureAllowed"

View file

@ -53,7 +53,6 @@ class FakeSettingsDiskSource : SettingsDiskSource {
private var storedIsIconLoadingDisabled: Boolean? = null
private var storedIsCrashLoggingEnabled: Boolean? = null
private var storedInitialAutofillDialogShown: Boolean? = null
private val storedApprovePasswordLoginsEnabled = mutableMapOf<String, Boolean?>()
private val storedScreenCaptureAllowed = mutableMapOf<String, Boolean?>()
private var storedSystemBiometricIntegritySource: String? = null
private val storedAccountBiometricIntegrityValidity = mutableMapOf<String, Boolean?>()
@ -247,16 +246,6 @@ class FakeSettingsDiskSource : SettingsDiskSource {
storedBlockedAutofillUris[userId] = blockedAutofillUris
}
override fun getApprovePasswordlessLoginsEnabled(userId: String): Boolean? =
storedApprovePasswordLoginsEnabled[userId]
override fun storeApprovePasswordlessLoginsEnabled(
userId: String,
isApprovePasswordlessLoginsEnabled: Boolean?,
) {
storedApprovePasswordLoginsEnabled[userId] = isApprovePasswordlessLoginsEnabled
}
override fun getScreenCaptureAllowed(userId: String): Boolean? =
storedScreenCaptureAllowed[userId]

View file

@ -10,8 +10,6 @@ import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
@ -51,8 +49,6 @@ class PushManagerTest {
private val authDiskSource: AuthDiskSource = FakeAuthDiskSource()
private val settingsDiskSource: SettingsDiskSource = FakeSettingsDiskSource()
private val pushDiskSource: PushDiskSource = PushDiskSourceImpl(FakeSharedPreferences())
private val pushService: PushService = mockk()
@ -63,7 +59,6 @@ class PushManagerTest {
fun setUp() {
pushManager = PushManagerImpl(
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
pushDiskSource = pushDiskSource,
pushService = pushService,
dispatcherManager = dispatcherManager,
@ -91,73 +86,33 @@ class PushManagerTest {
pushManager.onMessageReceived(INVALID_NOTIFICATION_JSON)
}
@Suppress("MaxLineLength")
@Test
fun `onMessageReceived auth request emits to nothing when getApprovePasswordlessLoginsEnabled is not true`() =
runTest {
settingsDiskSource.storeApprovePasswordlessLoginsEnabled(
userId = userId,
isApprovePasswordlessLoginsEnabled = false,
fun `onMessageReceived auth request emits to passwordlessRequestFlow`() = runTest {
pushManager.passwordlessRequestFlow.test {
pushManager.onMessageReceived(AUTH_REQUEST_NOTIFICATION_JSON)
assertEquals(
PasswordlessRequestData(
loginRequestId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
),
awaitItem(),
)
pushManager.passwordlessRequestFlow.test {
pushManager.onMessageReceived(AUTH_REQUEST_NOTIFICATION_JSON)
expectNoEvents()
}
}
}
@Suppress("MaxLineLength")
@Test
fun `onMessageReceived auth request emits to passwordlessRequestFlow when getApprovePasswordlessLoginsEnabled is true`() =
runTest {
settingsDiskSource.storeApprovePasswordlessLoginsEnabled(
userId = userId,
isApprovePasswordlessLoginsEnabled = true,
fun `onMessageReceived auth request response emits to passwordlessRequestFlow`() = runTest {
pushManager.passwordlessRequestFlow.test {
pushManager.onMessageReceived(AUTH_REQUEST_RESPONSE_NOTIFICATION_JSON)
assertEquals(
PasswordlessRequestData(
loginRequestId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
),
awaitItem(),
)
pushManager.passwordlessRequestFlow.test {
pushManager.onMessageReceived(AUTH_REQUEST_NOTIFICATION_JSON)
assertEquals(
PasswordlessRequestData(
loginRequestId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `onMessageReceived auth request response emits nothing when getApprovePasswordlessLoginsEnabled is not true`() =
runTest {
settingsDiskSource.storeApprovePasswordlessLoginsEnabled(
userId = userId,
isApprovePasswordlessLoginsEnabled = false,
)
pushManager.passwordlessRequestFlow.test {
pushManager.onMessageReceived(AUTH_REQUEST_RESPONSE_NOTIFICATION_JSON)
expectNoEvents()
}
}
@Suppress("MaxLineLength")
@Test
fun `onMessageReceived auth request response emits to passwordlessRequestFlow when getApprovePasswordlessLoginsEnabled is true`() =
runTest {
settingsDiskSource.storeApprovePasswordlessLoginsEnabled(
userId = userId,
isApprovePasswordlessLoginsEnabled = true,
)
pushManager.passwordlessRequestFlow.test {
pushManager.onMessageReceived(AUTH_REQUEST_RESPONSE_NOTIFICATION_JSON)
assertEquals(
PasswordlessRequestData(
loginRequestId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
),
awaitItem(),
)
}
}
}
@Test
fun `onMessageReceived logout should emit to logoutFlow`() = runTest {

View file

@ -905,31 +905,6 @@ class SettingsRepositoryTest {
}
}
@Test
fun `isApprovePasswordlessLoginsEnabled should properly update SettingsDiskSource`() {
fakeAuthDiskSource.userState = null
assertFalse(settingsRepository.isApprovePasswordlessLoginsEnabled)
fakeAuthDiskSource.userState = MOCK_USER_STATE
// Updates to the disk source change the repository value
fakeSettingsDiskSource.storeApprovePasswordlessLoginsEnabled(
userId = USER_ID,
isApprovePasswordlessLoginsEnabled = true,
)
assertEquals(
true,
settingsRepository.isApprovePasswordlessLoginsEnabled,
)
// Updates to the repository value change the disk source
settingsRepository.isApprovePasswordlessLoginsEnabled = false
assertEquals(
false,
fakeSettingsDiskSource.getApprovePasswordlessLoginsEnabled(userId = USER_ID),
)
}
@Test
fun `isScreenCaptureAllowed property should update SettingsDiskSource and emit changes`() =
runTest {

View file

@ -25,7 +25,6 @@ import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
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.FakePermissionManager
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import io.mockk.every
import io.mockk.just
@ -51,7 +50,6 @@ class AccountSecurityScreenTest : BaseComposeTest() {
every { startActivity(any()) } just runs
every { startApplicationDetailsSettingsActivity() } just runs
}
private val permissionsManager = FakePermissionManager()
private val captureBiometricsSuccess = slot<() -> Unit>()
private val captureBiometricsCancel = slot<() -> Unit>()
private val captureBiometricsLockOut = slot<() -> Unit>()
@ -84,7 +82,6 @@ class AccountSecurityScreenTest : BaseComposeTest() {
viewModel = viewModel,
biometricsManager = biometricsManager,
intentManager = intentManager,
permissionsManager = permissionsManager,
)
}
}
@ -95,211 +92,6 @@ class AccountSecurityScreenTest : BaseComposeTest() {
verify { viewModel.trySendAction(AccountSecurityAction.LogoutClick) }
}
@Suppress("MaxLineLength")
@Test
fun `on approve login requests toggle on should send PendingEnabled action and display dialog`() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Use this device to approve login requests made from other devices")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Approve login requests")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText(
"Use this device to approve login requests made from other devices",
)
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("No")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Yes")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
verify {
viewModel.trySendAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.PendingEnabled,
)
}
}
@Test
fun `on approve login requests toggle off should send Disabled action and hide requests row`() {
composeTestRule
.onNodeWithText("Pending login requests")
.assertDoesNotExist()
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = true) }
composeTestRule
.onNodeWithText("Use this device to approve login requests made from other devices")
.performScrollTo()
.performClick()
composeTestRule
.onNodeWithText("Pending login requests")
.performScrollTo()
.assertIsDisplayed()
verify {
viewModel.trySendAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled,
)
}
}
@Suppress("MaxLineLength")
@Test
fun `on approve login requests confirm Yes should send Enabled action and hide dialog when permission already granted`() {
permissionsManager.checkPermissionResult = true
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = false) }
composeTestRule
.onNodeWithText("Use this device to approve login requests made from other devices")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Yes")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule.assertNoDialogExists()
verify {
viewModel.trySendAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Enabled,
)
}
}
@Suppress("MaxLineLength")
@Test
fun `on approve login requests confirm Yes should send Enabled action and show permission dialog when permission not granted`() {
permissionsManager.checkPermissionResult = false
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = false) }
composeTestRule
.onNodeWithText("Use this device to approve login requests made from other devices")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Yes")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onAllNodesWithText("Receive push notifications for new login requests")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("No thanks")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Settings")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
verify {
viewModel.trySendAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Enabled,
)
}
}
@Test
fun `on approve login requests confirm No should send Disabled action and hide dialog`() {
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = false) }
composeTestRule
.onNodeWithText("Use this device to approve login requests made from other devices")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("No")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule.assertNoDialogExists()
verify {
viewModel.trySendAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled,
)
}
}
@Test
fun `on approve login requests should be toggled on or off according to state`() {
composeTestRule
.onNodeWithText("Use this device to approve login requests made from other devices")
.assertIsOff()
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = true) }
composeTestRule
.onNodeWithText("Use this device to approve login requests made from other devices")
.assertIsOn()
}
@Test
fun `on push permission dialog No thanks should hide dialog`() {
permissionsManager.checkPermissionResult = false
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = false) }
composeTestRule
.onNodeWithText("Use this device to approve login requests made from other devices")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Yes")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onAllNodesWithText("No thanks")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule.assertNoDialogExists()
}
@Test
fun `on push permission dialog Settings should hide dialog and send confirm action`() {
permissionsManager.checkPermissionResult = false
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = false) }
composeTestRule
.onNodeWithText("Use this device to approve login requests made from other devices")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Yes")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onAllNodesWithText("Settings")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule.assertNoDialogExists()
verify {
viewModel.trySendAction(AccountSecurityAction.PushNotificationConfirm)
}
}
@Test
fun `on NavigateToApplicationDataSettings should launch the correct intent`() {
mutableEventFlow.tryEmit(AccountSecurityEvent.NavigateToApplicationDataSettings)
@ -309,7 +101,6 @@ class AccountSecurityScreenTest : BaseComposeTest() {
@Test
fun `on pending login requests click should send PendingLoginRequestsClick`() {
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = true) }
composeTestRule
.onNodeWithText("Pending login requests")
.performScrollTo()
@ -1566,19 +1357,16 @@ class AccountSecurityScreenTest : BaseComposeTest() {
composeTestRule.onNodeWithText(rowText).assertDoesNotExist()
}
companion object {
private val DEFAULT_STATE = AccountSecurityState(
dialog = null,
fingerprintPhrase = "fingerprint-placeholder".asText(),
isApproveLoginRequestsEnabled = false,
isUnlockWithBiometricsEnabled = false,
isUnlockWithPasswordEnabled = true,
isUnlockWithPinEnabled = false,
vaultTimeout = VaultTimeout.ThirtyMinutes,
vaultTimeoutAction = VaultTimeoutAction.LOCK,
vaultTimeoutPolicyMinutes = null,
vaultTimeoutPolicyAction = null,
)
}
}
private val DEFAULT_STATE = AccountSecurityState(
dialog = null,
fingerprintPhrase = "fingerprint-placeholder".asText(),
isUnlockWithBiometricsEnabled = false,
isUnlockWithPasswordEnabled = true,
isUnlockWithPinEnabled = false,
vaultTimeout = VaultTimeout.ThirtyMinutes,
vaultTimeoutAction = VaultTimeoutAction.LOCK,
vaultTimeoutPolicyMinutes = null,
vaultTimeoutPolicyAction = null,
)

View file

@ -35,8 +35,6 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonObject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class AccountSecurityViewModelTest : BaseViewModelTest() {
@ -49,7 +47,6 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
private val vaultRepository: VaultRepository = mockk(relaxed = true)
private val settingsRepository: SettingsRepository = mockk {
every { isUnlockWithBiometricsEnabled } returns false
every { isApprovePasswordlessLoginsEnabled } returns false
every { isUnlockWithPinEnabled } returns false
every { vaultTimeout } returns VaultTimeout.ThirtyMinutes
every { vaultTimeoutAction } returns VaultTimeoutAction.LOCK
@ -530,54 +527,6 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
assertEquals(DEFAULT_STATE.copy(dialog = null), viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `on ApprovePasswordlessLoginsToggle enabled should update settings and set isApprovePasswordlessLoginsEnabled to true`() =
runTest {
every { settingsRepository.isApprovePasswordlessLoginsEnabled = true } just runs
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Enabled,
)
expectNoEvents()
verify(exactly = 1) { settingsRepository.isApprovePasswordlessLoginsEnabled = true }
}
assertTrue(viewModel.stateFlow.value.isApproveLoginRequestsEnabled)
}
@Suppress("MaxLineLength")
@Test
fun `on ApprovePasswordlessLoginsToggle pending enabled should set isApprovePasswordlessLoginsEnabled to true`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.PendingEnabled,
)
expectNoEvents()
}
assertTrue(viewModel.stateFlow.value.isApproveLoginRequestsEnabled)
}
@Suppress("MaxLineLength")
@Test
fun `on ApprovePasswordlessLoginsToggle disabled should update settings and set isApprovePasswordlessLoginsEnabled to false`() =
runTest {
every { settingsRepository.isApprovePasswordlessLoginsEnabled = false } just runs
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(
AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled,
)
expectNoEvents()
verify(exactly = 1) {
settingsRepository.isApprovePasswordlessLoginsEnabled = false
}
}
assertFalse(viewModel.stateFlow.value.isApproveLoginRequestsEnabled)
}
@Test
fun `on PushNotificationConfirm should send NavigateToApplicationDataSettings event`() =
runTest {
@ -616,7 +565,6 @@ private const val FINGERPRINT: String = "fingerprint"
private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState(
dialog = null,
fingerprintPhrase = FINGERPRINT.asText(),
isApproveLoginRequestsEnabled = false,
isUnlockWithBiometricsEnabled = false,
isUnlockWithPasswordEnabled = true,
isUnlockWithPinEnabled = false,

View file

@ -25,6 +25,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
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.FakePermissionManager
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
@ -68,6 +69,7 @@ class VaultScreenTest : BaseComposeTest() {
private var onNavigateToSearchScreen = false
private val exitManager = mockk<ExitManager>(relaxed = true)
private val intentManager = mockk<IntentManager>(relaxed = true)
private val permissionsManager = FakePermissionManager()
private val mutableEventFlow = bufferedMutableSharedFlow<VaultEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
@ -90,6 +92,7 @@ class VaultScreenTest : BaseComposeTest() {
onNavigateToSearchVault = { onNavigateToSearchScreen = true },
exitManager = exitManager,
intentManager = intentManager,
permissionsManager = permissionsManager,
)
}
}