Skip to content

Commit

Permalink
PM-14458: Update notifications permissions request (#4229)
Browse files Browse the repository at this point in the history
  • Loading branch information
david-livefront authored Nov 5, 2024
1 parent 202b4de commit 4930c10
Show file tree
Hide file tree
Showing 13 changed files with 369 additions and 89 deletions.
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,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(
Expand All @@ -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,
Expand All @@ -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() }
}
}
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.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
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.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
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 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@Composable
fun PendingRequestsScreen(
viewModel: PendingRequestsViewModel = hiltViewModel(),
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
onNavigateBack: () -> Unit,
onNavigateToLoginApproval: (fingerprint: String) -> Unit,
) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
}
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, animatedOnDismiss ->
ImportLoginsSuccessBottomSheetContent(
onCompleteImportLogins = hideSheetAndExecuteCompleteImportLogins,
onCompleteImportLogins = animatedOnDismiss,
modifier = Modifier.padding(paddingValues),
)
}
Expand Down
Loading

0 comments on commit 4930c10

Please sign in to comment.