Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PM-14458: Update notifications permissions request #4229

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -42,11 +45,20 @@ fun BitwardenModalBottomSheet(
modifier: Modifier = Modifier,
showBottomSheet: Boolean = true,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetContent: @Composable (PaddingValues) -> Unit,
sheetContent: @Composable (
paddingValues: PaddingValues,
onDismiss: () -> Unit,
) -> Unit,
) {
if (!showBottomSheet) return
val scope = rememberCoroutineScope()
val sheetDismiss: () -> Unit = {
scope
.launch { sheetState.hide() }
.invokeOnCompletion { onDismiss() }
}
ModalBottomSheet(
onDismissRequest = onDismiss,
onDismissRequest = sheetDismiss,
modifier = modifier,
dragHandle = null,
sheetState = sheetState,
Expand All @@ -62,7 +74,7 @@ fun BitwardenModalBottomSheet(
title = sheetTitle,
navigationIcon = NavigationIcon(
navigationIcon = rememberVectorPainter(R.drawable.ic_close),
onNavigationIconClick = onDismiss,
onNavigationIconClick = sheetDismiss,
navigationIconContentDescription = stringResource(R.string.close),
),
scrollBehavior = scrollBehavior,
Expand All @@ -73,7 +85,7 @@ fun BitwardenModalBottomSheet(
.nestedScroll(scrollBehavior.nestedScrollConnection)
.fillMaxSize(),
) { paddingValues ->
sheetContent(paddingValues)
sheetContent(paddingValues, sheetDismiss)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,13 +17,15 @@
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
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
Expand All @@ -40,18 +45,24 @@
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
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
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

/**
Expand All @@ -62,6 +73,7 @@
@Composable
fun PendingRequestsScreen(
viewModel: PendingRequestsViewModel = hiltViewModel(),
permissionsManager: PermissionsManager = LocalPermissionsManager.current,

Check warning on line 76 in app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt#L76

Added line #L76 was not covered by tests
onNavigateBack: () -> Unit,
onNavigateToLoginApproval: (fingerprint: String) -> Unit,
) {
Expand Down Expand Up @@ -98,6 +110,29 @@
}
}

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, onDismiss ->
PendingRequestsBottomSheetContent(
modifier = Modifier.padding(paddingValues),
permissionsManager = permissionsManager,
onDismiss = onDismiss,
)
}

val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
Expand Down Expand Up @@ -338,3 +373,65 @@
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 = "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))
Text(
text = stringResource(id = R.string.log_in_quickly_and_easily_across_devices),
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())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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() }
Expand All @@ -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 -> {
Expand Down Expand Up @@ -89,6 +92,10 @@ class PendingRequestsViewModel @Inject constructor(
}
}

private fun handleHideBottomSheet() {
mutableStateFlow.update { it.copy(hideBottomSheet = true) }
}

private fun handleOnLifecycleResumed() {
updateAuthRequestList()
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/"

Expand Down Expand Up @@ -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, onDismiss ->
ImportLoginsSuccessBottomSheetContent(
onCompleteImportLogins = hideSheetAndExecuteCompleteImportLogins,
onCompleteImportLogins = onDismiss,
modifier = Modifier.padding(paddingValues),
)
}
Expand Down
Loading