PM-14458: Update notifications permissions request (#4229)

This commit is contained in:
David Perez 2024-11-05 11:16:58 -06:00 committed by GitHub
parent 202b4de5ca
commit 4930c1032e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 369 additions and 89 deletions

View file

@ -9,6 +9,7 @@ import androidx.compose.material3.SheetState
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource 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.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme 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 * 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 sheetTitle The title to display in the [BitwardenTopAppBar]
* @param onDismiss The action to perform when the bottom sheet is dismissed will also be performed * @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 * 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 * @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. * the showing/hiding will be handled by the caller.
* @param sheetContent Content to display in the bottom sheet. The content is passed the padding * @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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -42,7 +45,10 @@ fun BitwardenModalBottomSheet(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showBottomSheet: Boolean = true, showBottomSheet: Boolean = true,
sheetState: SheetState = rememberModalBottomSheetState(), sheetState: SheetState = rememberModalBottomSheetState(),
sheetContent: @Composable (PaddingValues) -> Unit, sheetContent: @Composable (
paddingValues: PaddingValues,
animatedOnDismiss: () -> Unit,
) -> Unit,
) { ) {
if (!showBottomSheet) return if (!showBottomSheet) return
ModalBottomSheet( ModalBottomSheet(
@ -56,13 +62,14 @@ fun BitwardenModalBottomSheet(
shape = BitwardenTheme.shapes.bottomSheet, shape = BitwardenTheme.shapes.bottomSheet,
) { ) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val animatedOnDismiss = sheetState.createAnimatedDismissAction(onDismiss = onDismiss)
BitwardenScaffold( BitwardenScaffold(
topBar = { topBar = {
BitwardenTopAppBar( BitwardenTopAppBar(
title = sheetTitle, title = sheetTitle,
navigationIcon = NavigationIcon( navigationIcon = NavigationIcon(
navigationIcon = rememberVectorPainter(R.drawable.ic_close), navigationIcon = rememberVectorPainter(R.drawable.ic_close),
onNavigationIconClick = onDismiss, onNavigationIconClick = animatedOnDismiss,
navigationIconContentDescription = stringResource(R.string.close), navigationIconContentDescription = stringResource(R.string.close),
), ),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
@ -73,7 +80,18 @@ fun BitwardenModalBottomSheet(
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection)
.fillMaxSize(), .fillMaxSize(),
) { paddingValues -> ) { 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() }
}
}

View file

@ -1,5 +1,8 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests 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 android.widget.Toast
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable 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.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@ -21,6 +25,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -40,11 +45,15 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R 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.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect 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.bottomDivider
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin 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.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.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent 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.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter 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 import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/** /**
@ -62,6 +73,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@Composable @Composable
fun PendingRequestsScreen( fun PendingRequestsScreen(
viewModel: PendingRequestsViewModel = hiltViewModel(), viewModel: PendingRequestsViewModel = hiltViewModel(),
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToLoginApproval: (fingerprint: String) -> 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()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold( BitwardenScaffold(
modifier = Modifier modifier = Modifier
@ -338,3 +373,68 @@ private fun PendingRequestsEmpty(
Spacer(modifier = Modifier.height(64.dp)) 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())
}
}

View file

@ -27,6 +27,7 @@ private const val KEY_STATE = "state"
/** /**
* View model for the pending login requests screen. * View model for the pending login requests screen.
*/ */
@Suppress("TooManyFunctions")
@HiltViewModel @HiltViewModel
class PendingRequestsViewModel @Inject constructor( class PendingRequestsViewModel @Inject constructor(
private val clock: Clock, private val clock: Clock,
@ -39,6 +40,7 @@ class PendingRequestsViewModel @Inject constructor(
viewState = PendingRequestsState.ViewState.Loading, viewState = PendingRequestsState.ViewState.Loading,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value, isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
isRefreshing = false, isRefreshing = false,
hideBottomSheet = false,
), ),
) { ) {
private var authJob: Job = Job().apply { complete() } private var authJob: Job = Job().apply { complete() }
@ -56,6 +58,7 @@ class PendingRequestsViewModel @Inject constructor(
when (action) { when (action) {
PendingRequestsAction.CloseClick -> handleCloseClicked() PendingRequestsAction.CloseClick -> handleCloseClicked()
PendingRequestsAction.DeclineAllRequestsConfirm -> handleDeclineAllRequestsConfirmed() PendingRequestsAction.DeclineAllRequestsConfirm -> handleDeclineAllRequestsConfirmed()
PendingRequestsAction.HideBottomSheet -> handleHideBottomSheet()
PendingRequestsAction.LifecycleResume -> handleOnLifecycleResumed() PendingRequestsAction.LifecycleResume -> handleOnLifecycleResumed()
PendingRequestsAction.RefreshPull -> handleRefreshPull() PendingRequestsAction.RefreshPull -> handleRefreshPull()
is PendingRequestsAction.PendingRequestRowClick -> { is PendingRequestsAction.PendingRequestRowClick -> {
@ -89,6 +92,10 @@ class PendingRequestsViewModel @Inject constructor(
} }
} }
private fun handleHideBottomSheet() {
mutableStateFlow.update { it.copy(hideBottomSheet = true) }
}
private fun handleOnLifecycleResumed() { private fun handleOnLifecycleResumed() {
updateAuthRequestList() updateAuthRequestList()
} }
@ -193,6 +200,7 @@ data class PendingRequestsState(
val viewState: ViewState, val viewState: ViewState,
private val isPullToRefreshSettingEnabled: Boolean, private val isPullToRefreshSettingEnabled: Boolean,
val isRefreshing: Boolean, val isRefreshing: Boolean,
val hideBottomSheet: Boolean,
) : Parcelable { ) : Parcelable {
/** /**
* Indicates that the pull-to-refresh should be enabled in the UI. * Indicates that the pull-to-refresh should be enabled in the UI.
@ -297,6 +305,11 @@ sealed class PendingRequestsAction {
*/ */
data object DeclineAllRequestsConfirm : 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. * The screen has been re-opened and should be updated.
*/ */

View file

@ -23,7 +23,6 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll 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.handlers.rememberImportLoginHandler
import com.x8bit.bitwarden.ui.vault.feature.importlogins.model.InstructionStep import com.x8bit.bitwarden.ui.vault.feature.importlogins.model.InstructionStep
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
private const val IMPORT_HELP_URL = "https://bitwarden.com/help/import-data/" 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( BitwardenModalBottomSheet(
showBottomSheet = state.showBottomSheet, showBottomSheet = state.showBottomSheet,
sheetTitle = stringResource(R.string.bitwarden_tools), sheetTitle = stringResource(R.string.bitwarden_tools),
onDismiss = hideSheetAndExecuteCompleteImportLogins, onDismiss = handler.onSuccessfulSyncAcknowledged,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
modifier = Modifier.statusBarsPadding(), modifier = Modifier.statusBarsPadding(),
) { paddingValues -> ) { paddingValues, animatedOnDismiss ->
ImportLoginsSuccessBottomSheetContent( ImportLoginsSuccessBottomSheetContent(
onCompleteImportLogins = hideSheetAndExecuteCompleteImportLogins, onCompleteImportLogins = animatedOnDismiss,
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
) )
} }

View file

@ -1,7 +1,5 @@
package com.x8bit.bitwarden.ui.vault.feature.vault package com.x8bit.bitwarden.ui.vault.feature.vault
import android.Manifest
import android.annotation.SuppressLint
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
@ -15,7 +13,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager 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.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager 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.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.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction 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.feature.vault.handlers.VaultHandlers
@ -90,7 +85,6 @@ fun VaultScreen(
onNavigateToImportLogins: (SnackbarRelay) -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit,
exitManager: ExitManager = LocalExitManager.current, exitManager: ExitManager = LocalExitManager.current,
intentManager: IntentManager = LocalIntentManager.current, intentManager: IntentManager = LocalIntentManager.current,
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
@ -137,10 +131,6 @@ fun VaultScreen(
} }
} }
val vaultHandlers = remember(viewModel) { VaultHandlers.create(viewModel) } val vaultHandlers = remember(viewModel) { VaultHandlers.create(viewModel) }
VaultScreenPushNotifications(
hideNotificationsDialog = state.hideNotificationsDialog,
permissionsManager = permissionsManager,
)
VaultScreenScaffold( VaultScreenScaffold(
state = state, state = state,
pullToRefreshState = pullToRefreshState, 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] * Scaffold for the [VaultScreen]
*/ */

View file

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.vault.feature.vault package com.x8bit.bitwarden.ui.vault.feature.vault
import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.lifecycle.viewModelScope 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.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl 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.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
@ -102,7 +99,6 @@ class VaultViewModel @Inject constructor(
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value, isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
baseIconUrl = userState.activeAccount.environment.environmentUrlData.baseIconUrl, baseIconUrl = userState.activeAccount.environment.environmentUrlData.baseIconUrl,
hasMasterPassword = userState.activeAccount.hasMasterPassword, hasMasterPassword = userState.activeAccount.hasMasterPassword,
hideNotificationsDialog = isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU) || isFdroid,
isRefreshing = false, isRefreshing = false,
showImportActionCard = false, showImportActionCard = false,
showSshKeys = showSshKeys, showSshKeys = showSshKeys,
@ -713,7 +709,6 @@ data class VaultState(
private val isPullToRefreshSettingEnabled: Boolean, private val isPullToRefreshSettingEnabled: Boolean,
val baseIconUrl: String, val baseIconUrl: String,
val isIconLoadingDisabled: Boolean, val isIconLoadingDisabled: Boolean,
val hideNotificationsDialog: Boolean,
val isRefreshing: Boolean, val isRefreshing: Boolean,
val showImportActionCard: Boolean, val showImportActionCard: Boolean,
val showSshKeys: Boolean, val showSshKeys: Boolean,

View file

@ -0,0 +1,73 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="201dp"
android:viewportWidth="200"
android:viewportHeight="201">
<path
android:fillColor="#AAC3EF"
android:pathData="M0,38.17C0,31.26 5.6,25.67 12.5,25.67H125C131.9,25.67 137.5,31.26 137.5,38.17V117.33C137.5,124.24 131.9,129.83 125,129.83H12.5C5.6,129.83 0,124.24 0,117.33V38.17Z" />
<path
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M125,29.83H12.5C7.9,29.83 4.17,33.56 4.17,38.17V117.33C4.17,121.94 7.9,125.67 12.5,125.67H125C129.6,125.67 133.33,121.94 133.33,117.33V38.17C133.33,33.56 129.6,29.83 125,29.83ZM12.5,25.67C5.6,25.67 0,31.26 0,38.17V117.33C0,124.24 5.6,129.83 12.5,129.83H125C131.9,129.83 137.5,124.24 137.5,117.33V38.17C137.5,31.26 131.9,25.67 125,25.67H12.5Z" />
<path
android:fillColor="#79A1E9"
android:pathData="M47.92,75.67C47.92,72.21 50.71,69.42 54.17,69.42H83.33C86.78,69.42 89.58,72.21 89.58,75.67V96.5C89.58,99.95 86.78,102.75 83.33,102.75H54.17C50.71,102.75 47.92,99.95 47.92,96.5V75.67Z" />
<path
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M83.33,73.58H54.17C53.02,73.58 52.08,74.52 52.08,75.67V96.5C52.08,97.65 53.02,98.58 54.17,98.58H83.33C84.48,98.58 85.42,97.65 85.42,96.5V75.67C85.42,74.52 84.48,73.58 83.33,73.58ZM54.17,69.42C50.71,69.42 47.92,72.21 47.92,75.67V96.5C47.92,99.95 50.71,102.75 54.17,102.75H83.33C86.78,102.75 89.58,99.95 89.58,96.5V75.67C89.58,72.21 86.78,69.42 83.33,69.42H54.17Z" />
<path
android:fillColor="#175DDC"
android:pathData="M66.67,81.92C66.67,80.77 67.6,79.83 68.75,79.83C69.9,79.83 70.83,80.77 70.83,81.92V90.25C70.83,91.4 69.9,92.33 68.75,92.33C67.6,92.33 66.67,91.4 66.67,90.25V81.92Z" />
<path
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M58.33,67.33C58.33,61.58 63,56.92 68.75,56.92C74.5,56.92 79.17,61.58 79.17,67.33V69.42H75V67.33C75,63.88 72.2,61.08 68.75,61.08C65.3,61.08 62.5,63.88 62.5,67.33V69.42H58.33V67.33Z" />
<path
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M135.42,50.67H2.08V46.5H135.42V50.67Z" />
<path
android:fillColor="#175DDC"
android:pathData="M129.17,38.17C129.17,40.47 127.3,42.33 125,42.33C122.7,42.33 120.83,40.47 120.83,38.17C120.83,35.87 122.7,34 125,34C127.3,34 129.17,35.87 129.17,38.17Z" />
<path
android:fillColor="#175DDC"
android:pathData="M116.67,38.17C116.67,40.47 114.8,42.33 112.5,42.33C110.2,42.33 108.33,40.47 108.33,38.17C108.33,35.87 110.2,34 112.5,34C114.8,34 116.67,35.87 116.67,38.17Z" />
<path
android:fillColor="#175DDC"
android:pathData="M104.17,38.17C104.17,40.47 102.3,42.33 100,42.33C97.7,42.33 95.83,40.47 95.83,38.17C95.83,35.87 97.7,34 100,34C102.3,34 104.17,35.87 104.17,38.17Z" />
<path
android:fillColor="#F3F6F9"
android:pathData="M170.83,88.17C170.83,104.28 157.77,117.33 141.67,117.33C125.56,117.33 112.5,104.28 112.5,88.17C112.5,72.06 125.56,59 141.67,59C157.77,59 170.83,72.06 170.83,88.17Z" />
<path
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M141.67,113.17C155.47,113.17 166.67,101.97 166.67,88.17C166.67,74.36 155.47,63.17 141.67,63.17C127.86,63.17 116.67,74.36 116.67,88.17C116.67,101.97 127.86,113.17 141.67,113.17ZM141.67,117.33C157.77,117.33 170.83,104.28 170.83,88.17C170.83,72.06 157.77,59 141.67,59C125.56,59 112.5,72.06 112.5,88.17C112.5,104.28 125.56,117.33 141.67,117.33Z" />
<path
android:fillColor="#F3F6F9"
android:pathData="M195.64,150.62C198.52,157.57 200,165.02 200,172.54C200,174.27 198.6,175.67 196.87,175.67H88.54C86.82,175.67 85.42,174.27 85.42,172.54C85.42,165.02 86.9,157.57 89.78,150.62C92.66,143.67 96.88,137.35 102.2,132.03C107.52,126.71 113.83,122.49 120.78,119.61C127.73,116.73 135.18,115.25 142.71,115.25C150.23,115.25 157.68,116.73 164.63,119.61C171.58,122.49 177.9,126.71 183.22,132.03C188.54,137.35 192.76,143.67 195.64,150.62Z" />
<path
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M195.82,171.5C195.69,164.88 194.33,158.34 191.79,152.21C189.12,145.77 185.21,139.91 180.27,134.98C175.34,130.04 169.48,126.13 163.04,123.46C156.59,120.79 149.68,119.42 142.71,119.42C135.73,119.42 128.82,120.79 122.38,123.46C115.93,126.13 110.08,130.04 105.14,134.98C100.21,139.91 96.3,145.77 93.63,152.21C91.09,158.34 89.72,164.88 89.59,171.5H195.82ZM200,172.54C200,165.02 198.52,157.57 195.64,150.62C192.76,143.67 188.54,137.35 183.22,132.03C177.9,126.71 171.58,122.49 164.63,119.61C157.68,116.73 150.23,115.25 142.71,115.25C135.18,115.25 127.73,116.73 120.78,119.61C113.83,122.49 107.52,126.71 102.2,132.03C96.88,137.35 92.66,143.67 89.78,150.62C86.9,157.57 85.42,165.02 85.42,172.54C85.42,174.27 86.82,175.67 88.54,175.67H196.87C198.6,175.67 200,174.27 200,172.54Z" />
<path
android:fillColor="#FFBF00"
android:pathData="M22.92,127.75C22.92,118.54 30.38,111.08 39.58,111.08H95.83C105.04,111.08 112.5,118.54 112.5,127.75C112.5,136.95 105.04,144.42 95.83,144.42H39.58C30.38,144.42 22.92,136.95 22.92,127.75Z" />
<path
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M95.83,115.25H39.58C32.68,115.25 27.08,120.85 27.08,127.75C27.08,134.65 32.68,140.25 39.58,140.25H95.83C102.74,140.25 108.33,134.65 108.33,127.75C108.33,120.85 102.74,115.25 95.83,115.25ZM39.58,111.08C30.38,111.08 22.92,118.54 22.92,127.75C22.92,136.95 30.38,144.42 39.58,144.42H95.83C105.04,144.42 112.5,136.95 112.5,127.75C112.5,118.54 105.04,111.08 95.83,111.08H39.58Z" />
<path
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M41.68,120.61C42.83,120.61 43.76,121.54 43.76,122.69V125.61L46.49,124.71C47.59,124.36 48.76,124.95 49.12,126.05C49.48,127.14 48.88,128.32 47.79,128.67L45.02,129.58L46.76,132.01C47.42,132.95 47.21,134.25 46.27,134.92C45.33,135.59 44.03,135.37 43.36,134.43L41.68,132.07L39.99,134.43C39.32,135.37 38.02,135.59 37.08,134.92C36.15,134.25 35.93,132.95 36.6,132.01L38.33,129.58L35.56,128.67C34.47,128.32 33.87,127.14 34.23,126.05C34.59,124.95 35.77,124.36 36.86,124.71L39.59,125.61V122.69C39.59,121.54 40.53,120.61 41.68,120.61ZM60.43,120.61C61.58,120.61 62.51,121.54 62.51,122.69V125.61L65.24,124.71C66.34,124.36 67.51,124.95 67.87,126.05C68.23,127.14 67.63,128.32 66.54,128.67L63.77,129.58L65.51,132.01C66.17,132.95 65.96,134.25 65.02,134.92C64.08,135.59 62.78,135.37 62.11,134.43L60.43,132.07L58.74,134.43C58.07,135.37 56.77,135.59 55.83,134.92C54.9,134.25 54.68,132.95 55.35,132.01L57.08,129.58L54.31,128.67C53.22,128.32 52.62,127.14 52.98,126.05C53.34,124.95 54.52,124.36 55.61,124.71L58.34,125.61V122.69C58.34,121.54 59.28,120.61 60.43,120.61Z" />
<path
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M72.92,131.92C72.92,130.77 73.85,129.83 75,129.83L83.33,129.83C84.48,129.83 85.42,130.77 85.42,131.92C85.42,133.07 84.48,134 83.33,134L75,134C73.85,134 72.92,133.07 72.92,131.92Z" />
<path
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M89.58,131.92C89.58,130.77 90.52,129.83 91.67,129.83L100,129.83C101.15,129.83 102.08,130.77 102.08,131.92C102.08,133.07 101.15,134 100,134L91.67,134C90.52,134 89.58,133.07 89.58,131.92Z" />
</vector>

View file

@ -0,0 +1,73 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="201dp"
android:viewportWidth="200"
android:viewportHeight="201">
<path
android:fillColor="#DBE5F6"
android:pathData="M0,38.17C0,31.26 5.6,25.67 12.5,25.67H125C131.9,25.67 137.5,31.26 137.5,38.17V117.33C137.5,124.24 131.9,129.83 125,129.83H12.5C5.6,129.83 0,124.24 0,117.33V38.17Z" />
<path
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M125,29.83H12.5C7.9,29.83 4.17,33.56 4.17,38.17V117.33C4.17,121.94 7.9,125.67 12.5,125.67H125C129.6,125.67 133.33,121.94 133.33,117.33V38.17C133.33,33.56 129.6,29.83 125,29.83ZM12.5,25.67C5.6,25.67 0,31.26 0,38.17V117.33C0,124.24 5.6,129.83 12.5,129.83H125C131.9,129.83 137.5,124.24 137.5,117.33V38.17C137.5,31.26 131.9,25.67 125,25.67H12.5Z" />
<path
android:fillColor="#AAC3EF"
android:pathData="M47.92,75.67C47.92,72.21 50.71,69.42 54.17,69.42H83.33C86.78,69.42 89.58,72.21 89.58,75.67V96.5C89.58,99.95 86.78,102.75 83.33,102.75H54.17C50.71,102.75 47.92,99.95 47.92,96.5V75.67Z" />
<path
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M83.33,73.58H54.17C53.02,73.58 52.08,74.52 52.08,75.67V96.5C52.08,97.65 53.02,98.58 54.17,98.58H83.33C84.48,98.58 85.42,97.65 85.42,96.5V75.67C85.42,74.52 84.48,73.58 83.33,73.58ZM54.17,69.42C50.71,69.42 47.92,72.21 47.92,75.67V96.5C47.92,99.95 50.71,102.75 54.17,102.75H83.33C86.78,102.75 89.58,99.95 89.58,96.5V75.67C89.58,72.21 86.78,69.42 83.33,69.42H54.17Z" />
<path
android:fillColor="#020F66"
android:pathData="M66.67,81.92C66.67,80.77 67.6,79.83 68.75,79.83C69.9,79.83 70.83,80.77 70.83,81.92V90.25C70.83,91.4 69.9,92.33 68.75,92.33C67.6,92.33 66.67,91.4 66.67,90.25V81.92Z" />
<path
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M58.33,67.33C58.33,61.58 63,56.92 68.75,56.92C74.5,56.92 79.17,61.58 79.17,67.33V69.42H75V67.33C75,63.88 72.2,61.08 68.75,61.08C65.3,61.08 62.5,63.88 62.5,67.33V69.42H58.33V67.33Z" />
<path
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M135.42,50.67H2.08V46.5H135.42V50.67Z" />
<path
android:fillColor="#020F66"
android:pathData="M129.17,38.17C129.17,40.47 127.3,42.33 125,42.33C122.7,42.33 120.83,40.47 120.83,38.17C120.83,35.87 122.7,34 125,34C127.3,34 129.17,35.87 129.17,38.17Z" />
<path
android:fillColor="#020F66"
android:pathData="M116.67,38.17C116.67,40.47 114.8,42.33 112.5,42.33C110.2,42.33 108.33,40.47 108.33,38.17C108.33,35.87 110.2,34 112.5,34C114.8,34 116.67,35.87 116.67,38.17Z" />
<path
android:fillColor="#020F66"
android:pathData="M104.17,38.17C104.17,40.47 102.3,42.33 100,42.33C97.7,42.33 95.83,40.47 95.83,38.17C95.83,35.87 97.7,34 100,34C102.3,34 104.17,35.87 104.17,38.17Z" />
<path
android:fillColor="#ffffff"
android:pathData="M170.83,88.17C170.83,104.28 157.77,117.33 141.67,117.33C125.56,117.33 112.5,104.28 112.5,88.17C112.5,72.06 125.56,59 141.67,59C157.77,59 170.83,72.06 170.83,88.17Z" />
<path
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M141.67,113.17C155.47,113.17 166.67,101.97 166.67,88.17C166.67,74.36 155.47,63.17 141.67,63.17C127.86,63.17 116.67,74.36 116.67,88.17C116.67,101.97 127.86,113.17 141.67,113.17ZM141.67,117.33C157.77,117.33 170.83,104.28 170.83,88.17C170.83,72.06 157.77,59 141.67,59C125.56,59 112.5,72.06 112.5,88.17C112.5,104.28 125.56,117.33 141.67,117.33Z" />
<path
android:fillColor="#ffffff"
android:pathData="M195.64,150.62C198.52,157.57 200,165.02 200,172.54C200,174.27 198.6,175.67 196.87,175.67H88.54C86.82,175.67 85.42,174.27 85.42,172.54C85.42,165.02 86.9,157.57 89.78,150.62C92.66,143.67 96.88,137.35 102.2,132.03C107.52,126.71 113.83,122.49 120.78,119.61C127.73,116.73 135.18,115.25 142.71,115.25C150.23,115.25 157.68,116.73 164.63,119.61C171.58,122.49 177.9,126.71 183.22,132.03C188.54,137.35 192.76,143.67 195.64,150.62Z" />
<path
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M195.82,171.5C195.69,164.88 194.33,158.34 191.79,152.21C189.12,145.77 185.21,139.91 180.27,134.98C175.34,130.04 169.48,126.13 163.04,123.46C156.59,120.79 149.68,119.42 142.71,119.42C135.73,119.42 128.82,120.79 122.38,123.46C115.93,126.13 110.08,130.04 105.14,134.98C100.21,139.91 96.3,145.77 93.63,152.21C91.09,158.34 89.72,164.88 89.59,171.5H195.82ZM200,172.54C200,165.02 198.52,157.57 195.64,150.62C192.76,143.67 188.54,137.35 183.22,132.03C177.9,126.71 171.58,122.49 164.63,119.61C157.68,116.73 150.23,115.25 142.71,115.25C135.18,115.25 127.73,116.73 120.78,119.61C113.83,122.49 107.52,126.71 102.2,132.03C96.88,137.35 92.66,143.67 89.78,150.62C86.9,157.57 85.42,165.02 85.42,172.54C85.42,174.27 86.82,175.67 88.54,175.67H196.87C198.6,175.67 200,174.27 200,172.54Z" />
<path
android:fillColor="#FFBF00"
android:pathData="M22.92,127.75C22.92,118.54 30.38,111.08 39.58,111.08H95.83C105.04,111.08 112.5,118.54 112.5,127.75C112.5,136.95 105.04,144.42 95.83,144.42H39.58C30.38,144.42 22.92,136.95 22.92,127.75Z" />
<path
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M95.83,115.25H39.58C32.68,115.25 27.08,120.85 27.08,127.75C27.08,134.65 32.68,140.25 39.58,140.25H95.83C102.74,140.25 108.33,134.65 108.33,127.75C108.33,120.85 102.74,115.25 95.83,115.25ZM39.58,111.08C30.38,111.08 22.92,118.54 22.92,127.75C22.92,136.95 30.38,144.42 39.58,144.42H95.83C105.04,144.42 112.5,136.95 112.5,127.75C112.5,118.54 105.04,111.08 95.83,111.08H39.58Z" />
<path
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M41.67,120.61C42.83,120.61 43.76,121.54 43.76,122.69V125.61L46.49,124.71C47.58,124.36 48.76,124.95 49.12,126.05C49.48,127.14 48.88,128.32 47.79,128.67L45.02,129.58L46.75,132.01C47.42,132.95 47.2,134.25 46.27,134.92C45.33,135.59 44.03,135.37 43.36,134.43L41.67,132.07L39.99,134.43C39.32,135.37 38.02,135.59 37.08,134.92C36.14,134.25 35.93,132.95 36.6,132.01L38.33,129.58L35.56,128.67C34.47,128.32 33.87,127.14 34.23,126.05C34.59,124.95 35.76,124.36 36.86,124.71L39.59,125.61V122.69C39.59,121.54 40.52,120.61 41.67,120.61ZM60.42,120.61C61.58,120.61 62.51,121.54 62.51,122.69V125.61L65.24,124.71C66.33,124.36 67.51,124.95 67.87,126.05C68.23,127.14 67.63,128.32 66.54,128.67L63.77,129.58L65.5,132.01C66.17,132.95 65.95,134.25 65.02,134.92C64.08,135.59 62.78,135.37 62.11,134.43L60.42,132.07L58.74,134.43C58.07,135.37 56.77,135.59 55.83,134.92C54.89,134.25 54.68,132.95 55.35,132.01L57.08,129.58L54.31,128.67C53.22,128.32 52.62,127.14 52.98,126.05C53.34,124.95 54.51,124.36 55.61,124.71L58.34,125.61V122.69C58.34,121.54 59.27,120.61 60.42,120.61Z" />
<path
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M72.92,131.92C72.92,130.77 73.85,129.83 75,129.83L83.33,129.83C84.48,129.83 85.42,130.77 85.42,131.92C85.42,133.07 84.48,134 83.33,134L75,134C73.85,134 72.92,133.07 72.92,131.92Z" />
<path
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M89.58,131.92C89.58,130.77 90.52,129.83 91.67,129.83L100,129.83C101.15,129.83 102.08,130.77 102.08,131.92C102.08,133.07 101.15,134 100,134L91.67,134C90.52,134 89.58,133.07 89.58,131.92Z" />
</vector>

View file

@ -1082,4 +1082,8 @@ Do you want to switch to this account?</string>
<string name="ssh_keys">SSH keys</string> <string name="ssh_keys">SSH keys</string>
<string name="copy_public_key">Copy public key</string> <string name="copy_public_key">Copy public key</string>
<string name="copy_fingerprint">Copy fingerprint</string> <string name="copy_fingerprint">Copy fingerprint</string>
<string name="enable_notifications">Enable notifications</string>
<string name="log_in_quickly_and_easily_across_devices">Log in quickly and easily across devices</string>
<string name="bitwarden_can_notify_you_each_time_you_receive_a_new_login_request_from_another_device">Bitwarden can notify you each time you receive a new login request from another device.</string>
<string name="skip_for_now">Skip for now</string>
</resources> </resources>

View file

@ -1,18 +1,33 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests 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.assert
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick 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.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.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
@ -24,22 +39,38 @@ class PendingRequestsScreenTest : BaseComposeTest() {
private val mutableEventFlow = bufferedMutableSharedFlow<PendingRequestsEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<PendingRequestsEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<PendingRequestsViewModel>(relaxed = true) { private val viewModel = mockk<PendingRequestsViewModel> {
every { eventFlow } returns mutableEventFlow every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow every { stateFlow } returns mutableStateFlow
every { trySendAction(any()) } just runs
}
private val permissionsManager = FakePermissionManager().apply {
checkPermissionResult = false
shouldShowRequestRationale = true
} }
@Before @Before
fun setUp() { fun setUp() {
mockkStatic(::isFdroid)
mockkStatic(::isBuildVersionBelow)
every { isFdroid } returns false
every { isBuildVersionBelow(any()) } returns false
composeTestRule.setContent { composeTestRule.setContent {
PendingRequestsScreen( PendingRequestsScreen(
onNavigateBack = { onNavigateBackCalled = true }, onNavigateBack = { onNavigateBackCalled = true },
onNavigateToLoginApproval = { _ -> onNavigateToLoginApprovalCalled = true }, onNavigateToLoginApproval = { _ -> onNavigateToLoginApprovalCalled = true },
viewModel = viewModel, viewModel = viewModel,
permissionsManager = permissionsManager,
) )
} }
} }
@After
fun tearDown() {
unmockkStatic(::isFdroid)
unmockkStatic(::isBuildVersionBelow)
}
@Test @Test
fun `on NavigateBack should call onNavigateBack`() { fun `on NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(PendingRequestsEvent.NavigateBack) mutableEventFlow.tryEmit(PendingRequestsEvent.NavigateBack)
@ -70,6 +101,7 @@ class PendingRequestsScreenTest : BaseComposeTest() {
), ),
), ),
), ),
hideBottomSheet = true,
) )
composeTestRule.onNodeWithText("Decline all requests").performClick() composeTestRule.onNodeWithText("Decline all requests").performClick()
composeTestRule composeTestRule
@ -101,6 +133,7 @@ class PendingRequestsScreenTest : BaseComposeTest() {
), ),
), ),
), ),
hideBottomSheet = true,
) )
composeTestRule.onNodeWithText("Decline all requests").performClick() composeTestRule.onNodeWithText("Decline all requests").performClick()
composeTestRule composeTestRule
@ -114,12 +147,36 @@ class PendingRequestsScreenTest : BaseComposeTest() {
} }
} }
companion object { @Test
val DEFAULT_STATE: PendingRequestsState = PendingRequestsState( 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(), authRequests = emptyList(),
viewState = PendingRequestsState.ViewState.Loading, viewState = PendingRequestsState.ViewState.Loading,
isPullToRefreshSettingEnabled = false, isPullToRefreshSettingEnabled = false,
isRefreshing = false, isRefreshing = false,
) hideBottomSheet = false,
} )
}

View file

@ -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 @Test
fun `on RefreshPull should make auth request`() = runTest { fun `on RefreshPull should make auth request`() = runTest {
val viewModel = createViewModel() val viewModel = createViewModel()
@ -370,13 +377,12 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
savedStateHandle = SavedStateHandle().apply { set("state", state) }, savedStateHandle = SavedStateHandle().apply { set("state", state) },
) )
}
companion object { private val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
authRequests = emptyList(), authRequests = emptyList(),
viewState = PendingRequestsState.ViewState.Empty, viewState = PendingRequestsState.ViewState.Empty,
isPullToRefreshSettingEnabled = false, isPullToRefreshSettingEnabled = false,
isRefreshing = false, isRefreshing = false,
) hideBottomSheet = false,
} )
}

View file

@ -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.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager 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.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.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
@ -74,7 +73,6 @@ class VaultScreenTest : BaseComposeTest() {
private var onNavigateToSearchScreen = false private var onNavigateToSearchScreen = false
private val exitManager = mockk<ExitManager>(relaxed = true) private val exitManager = mockk<ExitManager>(relaxed = true)
private val intentManager = mockk<IntentManager>(relaxed = true) private val intentManager = mockk<IntentManager>(relaxed = true)
private val permissionsManager = FakePermissionManager()
private val mutableEventFlow = bufferedMutableSharedFlow<VaultEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<VaultEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
@ -101,7 +99,6 @@ class VaultScreenTest : BaseComposeTest() {
}, },
exitManager = exitManager, exitManager = exitManager,
intentManager = intentManager, 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 @Test
fun `action card for importing logins should show based on state`() { fun `action card for importing logins should show based on state`() {
mutableStateFlow.update { mutableStateFlow.update {
@ -1324,7 +1313,6 @@ private val DEFAULT_STATE: VaultState = VaultState(
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false, isIconLoadingDisabled = false,
hasMasterPassword = true, hasMasterPassword = true,
hideNotificationsDialog = true,
isRefreshing = false, isRefreshing = false,
showImportActionCard = false, showImportActionCard = false,
showSshKeys = false, showSshKeys = false,

View file

@ -1930,7 +1930,6 @@ private fun createMockVaultState(
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false, isIconLoadingDisabled = false,
hasMasterPassword = true, hasMasterPassword = true,
hideNotificationsDialog = true,
showImportActionCard = true, showImportActionCard = true,
isRefreshing = false, isRefreshing = false,
showSshKeys = showSshKeys, showSshKeys = showSshKeys,