diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/bottomsheet/BitwardenModalBottomSheet.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/bottomsheet/BitwardenModalBottomSheet.kt
index 505b6b445..85956def3 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/bottomsheet/BitwardenModalBottomSheet.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/bottomsheet/BitwardenModalBottomSheet.kt
@@ -9,6 +9,7 @@ import androidx.compose.material3.SheetState
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
@@ -19,6 +20,7 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
+import kotlinx.coroutines.launch
/**
* A reusable modal bottom sheet that applies provides a bottom sheet layout with the
@@ -28,11 +30,12 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param sheetTitle The title to display in the [BitwardenTopAppBar]
* @param onDismiss The action to perform when the bottom sheet is dismissed will also be performed
* when the "close" icon is clicked, caller must handle any desired animation or hiding of the
- * bottom sheet.
+ * bottom sheet. This will be invoked _after_ the sheet has been animated away.
* @param showBottomSheet Whether or not to show the bottom sheet, by default this is true assuming
* the showing/hiding will be handled by the caller.
* @param sheetContent Content to display in the bottom sheet. The content is passed the padding
- * from the containing [BitwardenScaffold].
+ * from the containing [BitwardenScaffold] and a `onDismiss` lambda to be used for manual dismissal
+ * that will include the dismissal animation.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -42,7 +45,10 @@ fun BitwardenModalBottomSheet(
modifier: Modifier = Modifier,
showBottomSheet: Boolean = true,
sheetState: SheetState = rememberModalBottomSheetState(),
- sheetContent: @Composable (PaddingValues) -> Unit,
+ sheetContent: @Composable (
+ paddingValues: PaddingValues,
+ animatedOnDismiss: () -> Unit,
+ ) -> Unit,
) {
if (!showBottomSheet) return
ModalBottomSheet(
@@ -56,13 +62,14 @@ fun BitwardenModalBottomSheet(
shape = BitwardenTheme.shapes.bottomSheet,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+ val animatedOnDismiss = sheetState.createAnimatedDismissAction(onDismiss = onDismiss)
BitwardenScaffold(
topBar = {
BitwardenTopAppBar(
title = sheetTitle,
navigationIcon = NavigationIcon(
navigationIcon = rememberVectorPainter(R.drawable.ic_close),
- onNavigationIconClick = onDismiss,
+ onNavigationIconClick = animatedOnDismiss,
navigationIconContentDescription = stringResource(R.string.close),
),
scrollBehavior = scrollBehavior,
@@ -73,7 +80,18 @@ fun BitwardenModalBottomSheet(
.nestedScroll(scrollBehavior.nestedScrollConnection)
.fillMaxSize(),
) { paddingValues ->
- sheetContent(paddingValues)
+ sheetContent(paddingValues, animatedOnDismiss)
}
}
}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SheetState.createAnimatedDismissAction(onDismiss: () -> Unit): () -> Unit {
+ val scope = rememberCoroutineScope()
+ return {
+ scope
+ .launch { this@createAnimatedDismissAction.hide() }
+ .invokeOnCompletion { onDismiss() }
+ }
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt
index 52d58190e..f38b56f25 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt
@@ -1,5 +1,8 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests
+import android.Manifest
+import android.annotation.SuppressLint
+import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
@@ -14,6 +17,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
@@ -21,6 +25,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
@@ -40,11 +45,15 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
+import com.x8bit.bitwarden.data.platform.util.isFdroid
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
+import com.x8bit.bitwarden.ui.platform.components.bottomsheet.BitwardenModalBottomSheet
+import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent
@@ -52,6 +61,8 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialo
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
+import com.x8bit.bitwarden.ui.platform.composition.LocalPermissionsManager
+import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -62,6 +73,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@Composable
fun PendingRequestsScreen(
viewModel: PendingRequestsViewModel = hiltViewModel(),
+ permissionsManager: PermissionsManager = LocalPermissionsManager.current,
onNavigateBack: () -> Unit,
onNavigateToLoginApproval: (fingerprint: String) -> Unit,
) {
@@ -98,6 +110,29 @@ fun PendingRequestsScreen(
}
}
+ val hideBottomSheet = state.hideBottomSheet ||
+ isFdroid ||
+ isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU) ||
+ permissionsManager.checkPermission(Manifest.permission.POST_NOTIFICATIONS) ||
+ !permissionsManager.shouldShowRequestPermissionRationale(
+ permission = Manifest.permission.POST_NOTIFICATIONS,
+ )
+ BitwardenModalBottomSheet(
+ showBottomSheet = !hideBottomSheet,
+ sheetTitle = stringResource(R.string.enable_notifications),
+ onDismiss = remember(viewModel) {
+ { viewModel.trySendAction(PendingRequestsAction.HideBottomSheet) }
+ },
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ modifier = Modifier.statusBarsPadding(),
+ ) { paddingValues, animatedOnDismiss ->
+ PendingRequestsBottomSheetContent(
+ modifier = Modifier.padding(paddingValues),
+ permissionsManager = permissionsManager,
+ onDismiss = animatedOnDismiss,
+ )
+ }
+
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
@@ -338,3 +373,68 @@ private fun PendingRequestsEmpty(
Spacer(modifier = Modifier.height(64.dp))
}
}
+
+@Composable
+private fun PendingRequestsBottomSheetContent(
+ permissionsManager: PermissionsManager,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val notificationPermissionLauncher = permissionsManager.getLauncher {
+ onDismiss()
+ }
+ Column(modifier = modifier.verticalScroll(rememberScrollState())) {
+ Spacer(modifier = Modifier.height(height = 24.dp))
+ Image(
+ painter = rememberVectorPainter(id = R.drawable.img_2fa),
+ contentDescription = null,
+ modifier = Modifier
+ .standardHorizontalMargin()
+ .size(size = 132.dp)
+ .align(alignment = Alignment.CenterHorizontally),
+ )
+ Spacer(modifier = Modifier.height(height = 24.dp))
+ Text(
+ text = stringResource(id = R.string.log_in_quickly_and_easily_across_devices),
+ style = BitwardenTheme.typography.titleMedium,
+ color = BitwardenTheme.colorScheme.text.primary,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+ Spacer(modifier = Modifier.height(height = 12.dp))
+ @Suppress("MaxLineLength")
+ Text(
+ text = stringResource(
+ id = R.string.bitwarden_can_notify_you_each_time_you_receive_a_new_login_request_from_another_device,
+ ),
+ style = BitwardenTheme.typography.bodyMedium,
+ color = BitwardenTheme.colorScheme.text.primary,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+ Spacer(modifier = Modifier.height(height = 24.dp))
+ BitwardenFilledButton(
+ label = stringResource(id = R.string.enable_notifications),
+ onClick = {
+ @SuppressLint("InlinedApi")
+ notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+ Spacer(modifier = Modifier.height(height = 12.dp))
+ BitwardenOutlinedButton(
+ label = stringResource(id = R.string.skip_for_now),
+ onClick = onDismiss,
+ modifier = Modifier
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+ Spacer(modifier = Modifier.navigationBarsPadding())
+ }
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt
index f4ac92d24..812b952eb 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt
@@ -27,6 +27,7 @@ private const val KEY_STATE = "state"
/**
* View model for the pending login requests screen.
*/
+@Suppress("TooManyFunctions")
@HiltViewModel
class PendingRequestsViewModel @Inject constructor(
private val clock: Clock,
@@ -39,6 +40,7 @@ class PendingRequestsViewModel @Inject constructor(
viewState = PendingRequestsState.ViewState.Loading,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
isRefreshing = false,
+ hideBottomSheet = false,
),
) {
private var authJob: Job = Job().apply { complete() }
@@ -56,6 +58,7 @@ class PendingRequestsViewModel @Inject constructor(
when (action) {
PendingRequestsAction.CloseClick -> handleCloseClicked()
PendingRequestsAction.DeclineAllRequestsConfirm -> handleDeclineAllRequestsConfirmed()
+ PendingRequestsAction.HideBottomSheet -> handleHideBottomSheet()
PendingRequestsAction.LifecycleResume -> handleOnLifecycleResumed()
PendingRequestsAction.RefreshPull -> handleRefreshPull()
is PendingRequestsAction.PendingRequestRowClick -> {
@@ -89,6 +92,10 @@ class PendingRequestsViewModel @Inject constructor(
}
}
+ private fun handleHideBottomSheet() {
+ mutableStateFlow.update { it.copy(hideBottomSheet = true) }
+ }
+
private fun handleOnLifecycleResumed() {
updateAuthRequestList()
}
@@ -193,6 +200,7 @@ data class PendingRequestsState(
val viewState: ViewState,
private val isPullToRefreshSettingEnabled: Boolean,
val isRefreshing: Boolean,
+ val hideBottomSheet: Boolean,
) : Parcelable {
/**
* Indicates that the pull-to-refresh should be enabled in the UI.
@@ -297,6 +305,11 @@ sealed class PendingRequestsAction {
*/
data object DeclineAllRequestsConfirm : PendingRequestsAction()
+ /**
+ * The user has dismissed the bottom sheet.
+ */
+ data object HideBottomSheet : PendingRequestsAction()
+
/**
* The screen has been re-opened and should be updated.
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt
index 4afe42fa8..e41ddeae4 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt
@@ -23,7 +23,6 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -65,7 +64,6 @@ import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.ImportLoginHan
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.rememberImportLoginHandler
import com.x8bit.bitwarden.ui.vault.feature.importlogins.model.InstructionStep
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.coroutines.launch
private const val IMPORT_HELP_URL = "https://bitwarden.com/help/import-data/"
@@ -100,27 +98,15 @@ fun ImportLoginsScreen(
}
}
- val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
- val scope = rememberCoroutineScope()
- val hideSheetAndExecuteCompleteImportLogins: () -> Unit = {
- // This pattern mirrors the onDismissRequest handling in the material ModalBottomSheet
- scope
- .launch {
- sheetState.hide()
- }
- .invokeOnCompletion {
- handler.onSuccessfulSyncAcknowledged()
- }
- }
BitwardenModalBottomSheet(
showBottomSheet = state.showBottomSheet,
sheetTitle = stringResource(R.string.bitwarden_tools),
- onDismiss = hideSheetAndExecuteCompleteImportLogins,
+ onDismiss = handler.onSuccessfulSyncAcknowledged,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
modifier = Modifier.statusBarsPadding(),
- ) { paddingValues ->
+ ) { paddingValues, animatedOnDismiss ->
ImportLoginsSuccessBottomSheetContent(
- onCompleteImportLogins = hideSheetAndExecuteCompleteImportLogins,
+ onCompleteImportLogins = animatedOnDismiss,
modifier = Modifier.padding(paddingValues),
)
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt
index 0bbded82d..04f3c078a 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt
@@ -1,7 +1,5 @@
package com.x8bit.bitwarden.ui.vault.feature.vault
-import android.Manifest
-import android.annotation.SuppressLint
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn
@@ -15,7 +13,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -61,11 +58,9 @@ import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnac
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.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers
@@ -90,7 +85,6 @@ fun VaultScreen(
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
exitManager: ExitManager = LocalExitManager.current,
intentManager: IntentManager = LocalIntentManager.current,
- permissionsManager: PermissionsManager = LocalPermissionsManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
@@ -137,10 +131,6 @@ fun VaultScreen(
}
}
val vaultHandlers = remember(viewModel) { VaultHandlers.create(viewModel) }
- VaultScreenPushNotifications(
- hideNotificationsDialog = state.hideNotificationsDialog,
- permissionsManager = permissionsManager,
- )
VaultScreenScaffold(
state = state,
pullToRefreshState = pullToRefreshState,
@@ -150,28 +140,6 @@ fun VaultScreen(
)
}
-/**
- * Handles the notifications permission request.
- */
-@Composable
-private fun VaultScreenPushNotifications(
- hideNotificationsDialog: Boolean,
- permissionsManager: PermissionsManager,
-) {
- if (hideNotificationsDialog) 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) {
- @SuppressLint("InlinedApi")
- // We check the version code as part of the 'hideNotificationsDialog' property.
- if (!permissionsManager.checkPermission(Manifest.permission.POST_NOTIFICATIONS)) {
- launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
- }
- }
-}
-
/**
* Scaffold for the [VaultScreen]
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt
index 879701c60..99b7e710c 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt
@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.vault.feature.vault
-import android.os.Build
import android.os.Parcelable
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.viewModelScope
@@ -19,8 +18,6 @@ import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
-import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
-import com.x8bit.bitwarden.data.platform.util.isFdroid
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
@@ -102,7 +99,6 @@ class VaultViewModel @Inject constructor(
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
baseIconUrl = userState.activeAccount.environment.environmentUrlData.baseIconUrl,
hasMasterPassword = userState.activeAccount.hasMasterPassword,
- hideNotificationsDialog = isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU) || isFdroid,
isRefreshing = false,
showImportActionCard = false,
showSshKeys = showSshKeys,
@@ -713,7 +709,6 @@ data class VaultState(
private val isPullToRefreshSettingEnabled: Boolean,
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
- val hideNotificationsDialog: Boolean,
val isRefreshing: Boolean,
val showImportActionCard: Boolean,
val showSshKeys: Boolean,
diff --git a/app/src/main/res/drawable-night/img_2fa.xml b/app/src/main/res/drawable-night/img_2fa.xml
new file mode 100644
index 000000000..2f2753f58
--- /dev/null
+++ b/app/src/main/res/drawable-night/img_2fa.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/img_2fa.xml b/app/src/main/res/drawable/img_2fa.xml
new file mode 100644
index 000000000..735a3c4a7
--- /dev/null
+++ b/app/src/main/res/drawable/img_2fa.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8949e5404..efd7faf15 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1082,4 +1082,8 @@ Do you want to switch to this account?
SSH keys
Copy public key
Copy fingerprint
+ Enable notifications
+ Log in quickly and easily across devices
+ Bitwarden can notify you each time you receive a new login request from another device.
+ Skip for now
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreenTest.kt
index 6e373b534..2311cc0b2 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreenTest.kt
@@ -1,18 +1,33 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests
+import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
+import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.isDialog
+import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollTo
+import androidx.compose.ui.test.performSemanticsAction
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
+import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
+import com.x8bit.bitwarden.data.platform.util.isFdroid
+import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
+import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import io.mockk.every
+import io.mockk.just
import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.runs
+import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertTrue
@@ -24,22 +39,38 @@ class PendingRequestsScreenTest : BaseComposeTest() {
private val mutableEventFlow = bufferedMutableSharedFlow()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
- private val viewModel = mockk(relaxed = true) {
+ private val viewModel = mockk {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
+ every { trySendAction(any()) } just runs
+ }
+ private val permissionsManager = FakePermissionManager().apply {
+ checkPermissionResult = false
+ shouldShowRequestRationale = true
}
@Before
fun setUp() {
+ mockkStatic(::isFdroid)
+ mockkStatic(::isBuildVersionBelow)
+ every { isFdroid } returns false
+ every { isBuildVersionBelow(any()) } returns false
composeTestRule.setContent {
PendingRequestsScreen(
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToLoginApproval = { _ -> onNavigateToLoginApprovalCalled = true },
viewModel = viewModel,
+ permissionsManager = permissionsManager,
)
}
}
+ @After
+ fun tearDown() {
+ unmockkStatic(::isFdroid)
+ unmockkStatic(::isBuildVersionBelow)
+ }
+
@Test
fun `on NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(PendingRequestsEvent.NavigateBack)
@@ -70,6 +101,7 @@ class PendingRequestsScreenTest : BaseComposeTest() {
),
),
),
+ hideBottomSheet = true,
)
composeTestRule.onNodeWithText("Decline all requests").performClick()
composeTestRule
@@ -101,6 +133,7 @@ class PendingRequestsScreenTest : BaseComposeTest() {
),
),
),
+ hideBottomSheet = true,
)
composeTestRule.onNodeWithText("Decline all requests").performClick()
composeTestRule
@@ -114,12 +147,36 @@ class PendingRequestsScreenTest : BaseComposeTest() {
}
}
- companion object {
- val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
- authRequests = emptyList(),
- viewState = PendingRequestsState.ViewState.Loading,
- isPullToRefreshSettingEnabled = false,
- isRefreshing = false,
- )
+ @Test
+ fun `on skip for now click should emit HideBottomSheet`() {
+ composeTestRule
+ .onNodeWithText(text = "Skip for now")
+ .performScrollTo()
+ .performSemanticsAction(SemanticsActions.OnClick)
+ dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 1000L)
+ verify(exactly = 1) {
+ viewModel.trySendAction(PendingRequestsAction.HideBottomSheet)
+ }
+ }
+
+ @Test
+ fun `on Enable notifications click should emit HideBottomSheet`() {
+ composeTestRule
+ .onAllNodesWithText(text = "Enable notifications")
+ .filterToOne(hasClickAction())
+ .performScrollTo()
+ .performSemanticsAction(SemanticsActions.OnClick)
+ dispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 1000L)
+ verify(exactly = 1) {
+ viewModel.trySendAction(PendingRequestsAction.HideBottomSheet)
+ }
}
}
+
+private val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
+ authRequests = emptyList(),
+ viewState = PendingRequestsState.ViewState.Loading,
+ isPullToRefreshSettingEnabled = false,
+ isRefreshing = false,
+ hideBottomSheet = false,
+)
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt
index bdea2ce7c..1637f4803 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt
@@ -165,6 +165,13 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
}
}
+ @Test
+ fun `on HideBottomSheet should make hideBottomSheet true`() {
+ val viewModel = createViewModel()
+ viewModel.trySendAction(PendingRequestsAction.HideBottomSheet)
+ assertEquals(DEFAULT_STATE.copy(hideBottomSheet = true), viewModel.stateFlow.value)
+ }
+
@Test
fun `on RefreshPull should make auth request`() = runTest {
val viewModel = createViewModel()
@@ -370,13 +377,12 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
settingsRepository = settingsRepository,
savedStateHandle = SavedStateHandle().apply { set("state", state) },
)
-
- companion object {
- val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
- authRequests = emptyList(),
- viewState = PendingRequestsState.ViewState.Empty,
- isPullToRefreshSettingEnabled = false,
- isRefreshing = false,
- )
- }
}
+
+private val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
+ authRequests = emptyList(),
+ viewState = PendingRequestsState.ViewState.Empty,
+ isPullToRefreshSettingEnabled = false,
+ isRefreshing = false,
+ hideBottomSheet = false,
+)
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt
index 203f9a8fe..ef752981f 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt
@@ -29,7 +29,6 @@ import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
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.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
@@ -74,7 +73,6 @@ class VaultScreenTest : BaseComposeTest() {
private var onNavigateToSearchScreen = false
private val exitManager = mockk(relaxed = true)
private val intentManager = mockk(relaxed = true)
- private val permissionsManager = FakePermissionManager()
private val mutableEventFlow = bufferedMutableSharedFlow()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
@@ -101,7 +99,6 @@ class VaultScreenTest : BaseComposeTest() {
},
exitManager = exitManager,
intentManager = intentManager,
- permissionsManager = permissionsManager,
)
}
}
@@ -1143,14 +1140,6 @@ class VaultScreenTest : BaseComposeTest() {
}
}
- @Test
- fun `permissionManager is invoked for notifications based on state`() {
- assertFalse(permissionsManager.hasGetLauncherBeenCalled)
- mutableStateFlow.update { it.copy(hideNotificationsDialog = false) }
- composeTestRule.waitForIdle()
- assertTrue(permissionsManager.hasGetLauncherBeenCalled)
- }
-
@Test
fun `action card for importing logins should show based on state`() {
mutableStateFlow.update {
@@ -1324,7 +1313,6 @@ private val DEFAULT_STATE: VaultState = VaultState(
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
hasMasterPassword = true,
- hideNotificationsDialog = true,
isRefreshing = false,
showImportActionCard = false,
showSshKeys = false,
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt
index d3071498d..fc86a0cf2 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt
@@ -1930,7 +1930,6 @@ private fun createMockVaultState(
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
hasMasterPassword = true,
- hideNotificationsDialog = true,
showImportActionCard = true,
isRefreshing = false,
showSshKeys = showSshKeys,