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,