Update the compose BOM to 2024.09.00 (#3874)

This commit is contained in:
David Perez 2024-09-06 12:45:12 -05:00 committed by GitHub
parent e468ec695b
commit 3f78ad6d6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 355 additions and 373 deletions

View file

@ -20,13 +20,13 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -308,7 +308,7 @@ private fun TermsAndPrivacySwitch(
} }
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = { onCheckedChange.invoke(!isChecked) }, onClick = { onCheckedChange.invoke(!isChecked) },
) )
.padding(start = 16.dp) .padding(start = 16.dp)

View file

@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -28,6 +27,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -448,7 +448,7 @@ private fun ReceiveMarketingEmailsSwitch(
} }
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = { onCheckedChange.invoke(!isChecked) }, onClick = { onCheckedChange.invoke(!isChecked) },
) )
.fillMaxWidth(), .fillMaxWidth(),

View file

@ -298,7 +298,6 @@ private fun TwoFactorLoginScreenContent(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@Preview(showBackground = true) @Preview(showBackground = true)
private fun TwoFactorLoginScreenContentPreview() { private fun TwoFactorLoginScreenContentPreview() {

View file

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.auth.feature.welcome package com.x8bit.bitwarden.ui.auth.feature.welcome
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -23,7 +22,6 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -57,7 +55,6 @@ private val LANDSCAPE_HORIZONTAL_MARGIN: Dp = 128.dp
/** /**
* Top level composable for the welcome screen. * Top level composable for the welcome screen.
*/ */
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun WelcomeScreen( fun WelcomeScreen(
onNavigateToCreateAccount: () -> Unit, onNavigateToCreateAccount: () -> Unit,
@ -103,7 +100,6 @@ fun WelcomeScreen(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun WelcomeScreenContent( private fun WelcomeScreenContent(
state: WelcomeState, state: WelcomeState,

View file

@ -2,8 +2,8 @@ package com.x8bit.bitwarden.ui.platform.base.util
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn

View file

@ -7,6 +7,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.CombinedModifier
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
@ -42,14 +43,17 @@ fun Modifier.scrolledContainerBackground(
): Modifier { ): Modifier {
val expandedColor = MaterialTheme.colorScheme.surface val expandedColor = MaterialTheme.colorScheme.surface
val collapsedColor = MaterialTheme.colorScheme.surfaceContainer val collapsedColor = MaterialTheme.colorScheme.surfaceContainer
return this then drawBehind { return CombinedModifier(
outer = this,
inner = drawBehind {
drawRect( drawRect(
color = topAppBarScrollBehavior.toScrolledContainerColor( color = topAppBarScrollBehavior.toScrolledContainerColor(
expandedColor = expandedColor, expandedColor = expandedColor,
collapsedColor = collapsedColor, collapsedColor = collapsedColor,
), ),
) )
} },
)
} }
/** /**

View file

@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
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.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -30,6 +29,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -276,7 +276,7 @@ private fun AccountSummaryItem(
.testTag("AccountCell") .testTag("AccountCell")
.combinedClickable( .combinedClickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = { onSwitchAccountClick(accountSummary) }, onClick = { onSwitchAccountClick(accountSummary) },
onLongClick = { onSwitchAccountLongClick(accountSummary) }, onLongClick = { onSwitchAccountLongClick(accountSummary) },
) )
@ -398,7 +398,7 @@ private fun AddAccountItem(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick, onClick = onClick,
) )
.padding(vertical = 8.dp) .padding(vertical = 8.dp)

View file

@ -4,9 +4,9 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -33,7 +33,7 @@ fun BitwardenBasicDialogRow(
modifier = modifier modifier = modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick, onClick = onClick,
) )
.padding( .padding(

View file

@ -6,10 +6,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -57,7 +57,7 @@ fun EnvironmentSelector(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(28.dp)) .clip(RoundedCornerShape(28.dp))
.clickable( .clickable(
indication = rememberRipple( indication = ripple(
bounded = true, bounded = true,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
), ),

View file

@ -6,10 +6,10 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
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.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -51,7 +51,7 @@ fun BitwardenGroupItem(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick, onClick = onClick,
) )
.bottomDivider( .bottomDivider(

View file

@ -10,11 +10,11 @@ import androidx.compose.foundation.layout.defaultMinSize
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.width import androidx.compose.foundation.layout.width
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -83,7 +83,7 @@ fun BitwardenListItem(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick, onClick = onClick,
) )
.defaultMinSize(minHeight = 72.dp) .defaultMinSize(minHeight = 72.dp)

View file

@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -50,7 +50,7 @@ fun BitwardenTextRow(
.clickable( .clickable(
enabled = isEnabled, enabled = isEnabled,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick, onClick = onClick,
) )
.semantics(mergeDescendants = true) { }, .semantics(mergeDescendants = true) { },

View file

@ -13,14 +13,15 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.contentColorFor import androidx.compose.material3.contentColorFor
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.semantics.testTagsAsResourceId
@ -37,7 +38,7 @@ fun BitwardenScaffold(
snackbarHost: @Composable () -> Unit = { }, snackbarHost: @Composable () -> Unit = { },
floatingActionButton: @Composable () -> Unit = { }, floatingActionButton: @Composable () -> Unit = { },
floatingActionButtonPosition: FabPosition = FabPosition.End, floatingActionButtonPosition: FabPosition = FabPosition.End,
pullToRefreshState: PullToRefreshState? = null, pullToRefreshState: BitwardenPullToRefreshState = rememberBitwardenPullToRefreshState(),
containerColor: Color = MaterialTheme.colorScheme.surface, containerColor: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = contentColorFor(containerColor), contentColor: Color = contentColorFor(containerColor),
contentWindowInsets: WindowInsets = ScaffoldDefaults contentWindowInsets: WindowInsets = ScaffoldDefaults
@ -48,7 +49,6 @@ fun BitwardenScaffold(
Scaffold( Scaffold(
modifier = Modifier modifier = Modifier
.semantics { testTagsAsResourceId = true } .semantics { testTagsAsResourceId = true }
.run { pullToRefreshState?.let { nestedScroll(it.nestedScrollConnection) } ?: this }
.then(modifier), .then(modifier),
topBar = topBar, topBar = topBar,
bottomBar = bottomBar, bottomBar = bottomBar,
@ -63,18 +63,50 @@ fun BitwardenScaffold(
contentColor = contentColor, contentColor = contentColor,
contentWindowInsets = contentWindowInsets, contentWindowInsets = contentWindowInsets,
content = { paddingValues -> content = { paddingValues ->
Box { val internalPullToRefreshState = rememberPullToRefreshState()
Box(
modifier = Modifier.pullToRefresh(
state = internalPullToRefreshState,
isRefreshing = pullToRefreshState.isRefreshing,
onRefresh = pullToRefreshState.onRefresh,
enabled = pullToRefreshState.isEnabled,
),
) {
content(paddingValues) content(paddingValues)
pullToRefreshState?.let { PullToRefreshDefaults.Indicator(
PullToRefreshContainer(
state = it,
modifier = Modifier modifier = Modifier
.padding(paddingValues) .padding(paddingValues)
.align(Alignment.TopCenter), .align(Alignment.TopCenter),
isRefreshing = pullToRefreshState.isRefreshing,
state = internalPullToRefreshState,
) )
} }
}
}, },
) )
} }
/**
* The state of the pull-to-refresh.
*/
data class BitwardenPullToRefreshState(
val isEnabled: Boolean,
val isRefreshing: Boolean,
val onRefresh: () -> Unit,
)
/**
* Create and remember the default [BitwardenPullToRefreshState].
*/
@Composable
fun rememberBitwardenPullToRefreshState(
isEnabled: Boolean = false,
isRefreshing: Boolean = false,
onRefresh: () -> Unit = { },
): BitwardenPullToRefreshState = remember(isEnabled, isRefreshing, onRefresh) {
BitwardenPullToRefreshState(
isEnabled = isEnabled,
isRefreshing = isRefreshing,
onRefresh = onRefresh,
)
}

View file

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.platform.components.segment package com.x8bit.bitwarden.ui.platform.components.segment
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MultiChoiceSegmentedButtonRow import androidx.compose.material3.MultiChoiceSegmentedButtonRow
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
@ -16,7 +15,6 @@ import kotlinx.collections.immutable.ImmutableList
* @param options List of options to display. * @param options List of options to display.
* @param modifier Modifier. * @param modifier Modifier.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BitwardenSegmentedButton( fun BitwardenSegmentedButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View file

@ -5,9 +5,9 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -41,7 +41,7 @@ fun BitwardenClickableText(
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(cornerSize)) .clip(RoundedCornerShape(cornerSize))
.clickable( .clickable(
indication = rememberRipple( indication = ripple(
bounded = true, bounded = true,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
), ),

View file

@ -10,10 +10,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -49,7 +49,7 @@ fun BitwardenSwitch(
if (onCheckedChange != null) { if (onCheckedChange != null) {
this.clickable( this.clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = { onCheckedChange.invoke(!isChecked) }, onClick = { onCheckedChange.invoke(!isChecked) },
) )
} else { } else {

View file

@ -5,11 +5,11 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -49,7 +49,7 @@ fun BitwardenSwitchWithActions(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = { onCheckedChange?.invoke(!isChecked) }, onClick = { onCheckedChange?.invoke(!isChecked) },
) )
.semantics(mergeDescendants = true) { .semantics(mergeDescendants = true) {

View file

@ -10,10 +10,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -57,7 +57,7 @@ fun BitwardenWideSwitch(
.wrapContentHeight() .wrapContentHeight()
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = { onCheckedChange?.invoke(!isChecked) }, onClick = { onCheckedChange?.invoke(!isChecked) },
enabled = !readOnly && enabled, enabled = !readOnly && enabled,
) )

View file

@ -12,13 +12,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -108,7 +108,7 @@ private fun SettingsRow(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick, onClick = onClick,
) )
.bottomDivider(paddingStart = 16.dp) .bottomDivider(paddingStart = 16.dp)

View file

@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -22,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -249,7 +249,7 @@ private fun CopyRow(
modifier = modifier modifier = modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick, onClick = onClick,
) )
.semantics(mergeDescendants = true) { .semantics(mergeDescendants = true) {

View file

@ -17,19 +17,16 @@ 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
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
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
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -52,6 +49,7 @@ 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
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog 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.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.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
@ -70,17 +68,15 @@ fun PendingRequestsScreen(
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
val resources = context.resources val resources = context.resources
val pullToRefreshState by rememberUpdatedState( val pullToRefreshState = rememberBitwardenPullToRefreshState(
newValue = rememberPullToRefreshState().takeIf { state.isPullToRefreshEnabled }, isEnabled = state.isPullToRefreshEnabled,
isRefreshing = state.isRefreshing,
onRefresh = remember(viewModel) {
{ viewModel.trySendAction(PendingRequestsAction.RefreshPull) }
},
) )
LaunchedEffect(key1 = pullToRefreshState?.isRefreshing) {
if (pullToRefreshState?.isRefreshing == true) {
viewModel.trySendAction(PendingRequestsAction.RefreshPull)
}
}
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
PendingRequestsEvent.DismissPullToRefresh -> pullToRefreshState?.endRefresh()
PendingRequestsEvent.NavigateBack -> onNavigateBack() PendingRequestsEvent.NavigateBack -> onNavigateBack()
is PendingRequestsEvent.NavigateToLoginApproval -> { is PendingRequestsEvent.NavigateToLoginApproval -> {
onNavigateToLoginApproval(event.fingerprint) onNavigateToLoginApproval(event.fingerprint)
@ -244,7 +240,7 @@ private fun PendingRequestItem(
modifier = modifier modifier = modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = { onNavigateToLoginApproval(fingerprintPhrase) }, onClick = { onNavigateToLoginApproval(fingerprintPhrase) },
), ),
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,

View file

@ -38,6 +38,7 @@ class PendingRequestsViewModel @Inject constructor(
authRequests = emptyList(), authRequests = emptyList(),
viewState = PendingRequestsState.ViewState.Loading, viewState = PendingRequestsState.ViewState.Loading,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value, isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
isRefreshing = false,
), ),
) { ) {
private var authJob: Job = Job().apply { complete() } private var authJob: Job = Job().apply { complete() }
@ -93,6 +94,7 @@ class PendingRequestsViewModel @Inject constructor(
} }
private fun handleRefreshPull() { private fun handleRefreshPull() {
mutableStateFlow.update { it.copy(isRefreshing = true) }
updateAuthRequestList() updateAuthRequestList()
} }
@ -169,7 +171,7 @@ class PendingRequestsViewModel @Inject constructor(
} }
} }
} }
sendEvent(PendingRequestsEvent.DismissPullToRefresh) mutableStateFlow.update { it.copy(isRefreshing = false) }
} }
private fun updateAuthRequestList() { private fun updateAuthRequestList() {
@ -190,6 +192,7 @@ data class PendingRequestsState(
val authRequests: List<AuthRequest>, val authRequests: List<AuthRequest>,
val viewState: ViewState, val viewState: ViewState,
private val isPullToRefreshSettingEnabled: Boolean, private val isPullToRefreshSettingEnabled: Boolean,
val isRefreshing: 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.
@ -259,11 +262,6 @@ data class PendingRequestsState(
* Models events for the delete account screen. * Models events for the delete account screen.
*/ */
sealed class PendingRequestsEvent { sealed class PendingRequestsEvent {
/**
* Dismisses the pull-to-refresh indicator.
*/
data object DismissPullToRefresh : PendingRequestsEvent()
/** /**
* Navigates back. * Navigates back.
*/ */

View file

@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
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.material.ripple.rememberRipple
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -28,6 +27,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -280,7 +280,7 @@ private fun BlockAutoFillListItem(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick, onClick = onClick,
) )
.bottomDivider(paddingStart = 16.dp) .bottomDivider(paddingStart = 16.dp)

View file

@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
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.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -20,6 +19,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -168,7 +168,7 @@ private fun FoldersContent(
.testTag("FolderCell") .testTag("FolderCell")
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = { onItemClick(it.id) }, onClick = { onItemClick(it.id) },
) )
.bottomDivider(paddingStart = 16.dp) .bottomDivider(paddingStart = 16.dp)

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
@ -142,7 +141,6 @@ fun VaultUnlockedNavBarScreen(
* Scaffold that contains the bottom nav bar for the [VaultUnlockedNavBarScreen] * Scaffold that contains the bottom nav bar for the [VaultUnlockedNavBarScreen]
*/ */
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod") @Suppress("LongMethod")
private fun VaultUnlockedNavBarScaffold( private fun VaultUnlockedNavBarScaffold(
state: VaultUnlockedNavBarState, state: VaultUnlockedNavBarState,

View file

@ -12,10 +12,8 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
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.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -39,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
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.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
@ -63,18 +62,16 @@ fun SendScreen(
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
val pullToRefreshState = rememberPullToRefreshState() val pullToRefreshState = rememberBitwardenPullToRefreshState(
.takeIf { state.isPullToRefreshEnabled } isEnabled = state.isPullToRefreshEnabled,
LaunchedEffect(key1 = pullToRefreshState?.isRefreshing) { isRefreshing = state.isRefreshing,
if (pullToRefreshState?.isRefreshing == true) { onRefresh = remember(viewModel) {
viewModel.trySendAction(SendAction.RefreshPull) { viewModel.trySendAction(SendAction.RefreshPull) }
} },
} )
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
is SendEvent.DismissPullToRefresh -> pullToRefreshState?.endRefresh()
is SendEvent.NavigateToSearch -> onNavigateToSearchSend(SearchType.Sends.All) is SendEvent.NavigateToSearch -> onNavigateToSearchSend(SearchType.Sends.All)
is SendEvent.NavigateNewSend -> onNavigateToAddSend() is SendEvent.NavigateNewSend -> onNavigateToAddSend()

View file

@ -56,6 +56,7 @@ class SendViewModel @Inject constructor(
policyDisablesSend = policyManager policyDisablesSend = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) .getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
.any(), .any(),
isRefreshing = false,
), ),
) { ) {
@ -174,9 +175,9 @@ class SendViewModel @Inject constructor(
message = R.string.generic_error_message.asText(), message = R.string.generic_error_message.asText(),
), ),
dialogState = null, dialogState = null,
isRefreshing = false,
) )
} }
sendEvent(SendEvent.DismissPullToRefresh)
} }
is DataState.Loaded -> { is DataState.Loaded -> {
@ -189,9 +190,9 @@ class SendViewModel @Inject constructor(
.baseWebSendUrl, .baseWebSendUrl,
), ),
dialogState = null, dialogState = null,
isRefreshing = false,
) )
} }
sendEvent(SendEvent.DismissPullToRefresh)
} }
DataState.Loading -> { DataState.Loading -> {
@ -212,9 +213,9 @@ class SendViewModel @Inject constructor(
), ),
), ),
dialogState = null, dialogState = null,
isRefreshing = false,
) )
} }
sendEvent(SendEvent.DismissPullToRefresh)
} }
is DataState.Pending -> { is DataState.Pending -> {
@ -317,6 +318,7 @@ class SendViewModel @Inject constructor(
} }
private fun handleRefreshPull() { private fun handleRefreshPull() {
mutableStateFlow.update { it.copy(isRefreshing = true) }
// The Pull-To-Refresh composable is already in the refreshing state. // The Pull-To-Refresh composable is already in the refreshing state.
// We will reset that state when sendDataStateFlow emits later on. // We will reset that state when sendDataStateFlow emits later on.
vaultRepo.sync() vaultRepo.sync()
@ -332,6 +334,7 @@ data class SendState(
val dialogState: DialogState?, val dialogState: DialogState?,
private val isPullToRefreshSettingEnabled: Boolean, private val isPullToRefreshSettingEnabled: Boolean,
val policyDisablesSend: Boolean, val policyDisablesSend: Boolean,
val isRefreshing: Boolean,
) : Parcelable { ) : Parcelable {
/** /**
@ -573,11 +576,6 @@ sealed class SendAction {
* Models events for the send screen. * Models events for the send screen.
*/ */
sealed class SendEvent { sealed class SendEvent {
/**
* Dismisses the pull-to-refresh indicator.
*/
data object DismissPullToRefresh : SendEvent()
/** /**
* Navigate to the new send screen. * Navigate to the new send screen.
*/ */

View file

@ -8,11 +8,8 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
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
@ -45,7 +42,9 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPassword
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwritePasskeyConfirmationDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwritePasskeyConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenPinDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenPinDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenPullToRefreshState
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.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.composition.LocalFido2CompletionManager import com.x8bit.bitwarden.ui.platform.composition.LocalFido2CompletionManager
@ -65,7 +64,6 @@ import kotlinx.collections.immutable.toImmutableList
/** /**
* Displays the vault item listing screen. * Displays the vault item listing screen.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
fun VaultItemListingScreen( fun VaultItemListingScreen(
@ -89,18 +87,17 @@ fun VaultItemListingScreen(
VaultItemListingUserVerificationHandlers.create(viewModel = viewModel) VaultItemListingUserVerificationHandlers.create(viewModel = viewModel)
} }
val pullToRefreshState = rememberPullToRefreshState().takeIf { state.isPullToRefreshEnabled } val pullToRefreshState = rememberBitwardenPullToRefreshState(
LaunchedEffect(key1 = pullToRefreshState?.isRefreshing) { isEnabled = state.isPullToRefreshEnabled,
if (pullToRefreshState?.isRefreshing == true) { isRefreshing = state.isRefreshing,
viewModel.trySendAction(VaultItemListingsAction.RefreshPull) onRefresh = remember(viewModel) {
} { viewModel.trySendAction(VaultItemListingsAction.RefreshPull) }
} },
)
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
is VaultItemListingEvent.NavigateBack -> onNavigateBack() is VaultItemListingEvent.NavigateBack -> onNavigateBack()
is VaultItemListingEvent.DismissPullToRefresh -> pullToRefreshState?.endRefresh()
is VaultItemListingEvent.NavigateToVaultItem -> { is VaultItemListingEvent.NavigateToVaultItem -> {
onNavigateToVaultItem(event.id) onNavigateToVaultItem(event.id)
} }
@ -388,7 +385,7 @@ private fun VaultItemListingDialogs(
@Composable @Composable
private fun VaultItemListingScaffold( private fun VaultItemListingScaffold(
state: VaultItemListingState, state: VaultItemListingState,
pullToRefreshState: PullToRefreshState?, pullToRefreshState: BitwardenPullToRefreshState,
vaultItemListingHandlers: VaultItemListingHandlers, vaultItemListingHandlers: VaultItemListingHandlers,
) { ) {
var isAccountMenuVisible by rememberSaveable { mutableStateOf(false) } var isAccountMenuVisible by rememberSaveable { mutableStateOf(false) }

View file

@ -133,6 +133,7 @@ class VaultItemListingViewModel @Inject constructor(
fido2CredentialAssertionRequest = fido2AssertionData?.fido2AssertionRequest, fido2CredentialAssertionRequest = fido2AssertionData?.fido2AssertionRequest,
fido2GetCredentialsRequest = fido2GetCredentialsData?.fido2GetCredentialsRequest, fido2GetCredentialsRequest = fido2GetCredentialsData?.fido2GetCredentialsRequest,
isPremium = userState.activeAccount.isPremium, isPremium = userState.activeAccount.isPremium,
isRefreshing = false,
) )
}, },
) { ) {
@ -298,6 +299,7 @@ class VaultItemListingViewModel @Inject constructor(
} }
private fun handleRefreshPull() { private fun handleRefreshPull() {
mutableStateFlow.update { it.copy(isRefreshing = true) }
// The Pull-To-Refresh composable is already in the refreshing state. // The Pull-To-Refresh composable is already in the refreshing state.
// We will reset that state when sendDataStateFlow emits later on. // We will reset that state when sendDataStateFlow emits later on.
vaultRepository.sync() vaultRepository.sync()
@ -1232,7 +1234,7 @@ class VaultItemListingViewModel @Inject constructor(
) )
} }
} }
sendEvent(VaultItemListingEvent.DismissPullToRefresh) mutableStateFlow.update { it.copy(isRefreshing = false) }
} }
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) { private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
@ -1261,7 +1263,7 @@ class VaultItemListingViewModel @Inject constructor(
), ),
) )
} }
?: sendEvent(VaultItemListingEvent.DismissPullToRefresh) ?: mutableStateFlow.update { it.copy(isRefreshing = false) }
} }
private fun vaultLoadingReceive() { private fun vaultLoadingReceive() {
@ -1286,7 +1288,7 @@ class VaultItemListingViewModel @Inject constructor(
) )
} }
} }
sendEvent(VaultItemListingEvent.DismissPullToRefresh) mutableStateFlow.update { it.copy(isRefreshing = false) }
} }
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) { private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
@ -1640,6 +1642,7 @@ data class VaultItemListingState(
val fido2GetCredentialsRequest: Fido2GetCredentialsRequest? = null, val fido2GetCredentialsRequest: Fido2GetCredentialsRequest? = null,
val hasMasterPassword: Boolean, val hasMasterPassword: Boolean,
val isPremium: Boolean, val isPremium: Boolean,
val isRefreshing: Boolean,
) { ) {
/** /**
* Whether or not this represents a listing screen for autofill. * Whether or not this represents a listing screen for autofill.
@ -2027,11 +2030,6 @@ data class VaultItemListingState(
* Models events for the [VaultItemListingScreen]. * Models events for the [VaultItemListingScreen].
*/ */
sealed class VaultItemListingEvent { sealed class VaultItemListingEvent {
/**
* Dismisses the pull-to-refresh indicator.
*/
data object DismissPullToRefresh : VaultItemListingEvent()
/** /**
* Navigates to the Create Account screen. * Navigates to the Create Account screen.
*/ */

View file

@ -16,8 +16,6 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
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.LaunchedEffect
@ -52,7 +50,9 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenPullToRefreshState
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.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
@ -70,7 +70,6 @@ import kotlinx.collections.immutable.toImmutableList
/** /**
* The vault screen for the application. * The vault screen for the application.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
fun VaultScreen( fun VaultScreen(
@ -88,16 +87,15 @@ fun VaultScreen(
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
val pullToRefreshState = rememberPullToRefreshState().takeIf { state.isPullToRefreshEnabled } val pullToRefreshState = rememberBitwardenPullToRefreshState(
LaunchedEffect(key1 = pullToRefreshState?.isRefreshing) { isEnabled = state.isPullToRefreshEnabled,
if (pullToRefreshState?.isRefreshing == true) { isRefreshing = state.isRefreshing,
viewModel.trySendAction(VaultAction.RefreshPull) onRefresh = remember(viewModel) {
} { viewModel.trySendAction(VaultAction.RefreshPull) }
} },
)
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
VaultEvent.DismissPullToRefresh -> pullToRefreshState?.endRefresh()
VaultEvent.NavigateToAddItemScreen -> onNavigateToVaultAddItemScreen() VaultEvent.NavigateToAddItemScreen -> onNavigateToVaultAddItemScreen()
VaultEvent.NavigateToVaultSearchScreen -> onNavigateToSearchVault(SearchType.Vault.All) VaultEvent.NavigateToVaultSearchScreen -> onNavigateToSearchVault(SearchType.Vault.All)
@ -167,7 +165,7 @@ private fun VaultScreenPushNotifications(
@Composable @Composable
private fun VaultScreenScaffold( private fun VaultScreenScaffold(
state: VaultState, state: VaultState,
pullToRefreshState: PullToRefreshState?, pullToRefreshState: BitwardenPullToRefreshState,
vaultHandlers: VaultHandlers, vaultHandlers: VaultHandlers,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit, onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
) { ) {

View file

@ -91,6 +91,7 @@ class VaultViewModel @Inject constructor(
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, hideNotificationsDialog = isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU) || isFdroid,
isRefreshing = false,
) )
}, },
) { ) {
@ -298,6 +299,7 @@ class VaultViewModel @Inject constructor(
} }
private fun handleRefreshPull() { private fun handleRefreshPull() {
mutableStateFlow.update { it.copy(isRefreshing = true) }
// The Pull-To-Refresh composable is already in the refreshing state. // The Pull-To-Refresh composable is already in the refreshing state.
// We will reset that state when sendDataStateFlow emits later on. // We will reset that state when sendDataStateFlow emits later on.
vaultRepository.sync() vaultRepository.sync()
@ -510,8 +512,8 @@ class VaultViewModel @Inject constructor(
hasMasterPassword = state.hasMasterPassword, hasMasterPassword = state.hasMasterPassword,
errorTitle = R.string.an_error_has_occurred.asText(), errorTitle = R.string.an_error_has_occurred.asText(),
errorMessage = R.string.generic_error_message.asText(), errorMessage = R.string.generic_error_message.asText(),
isRefreshing = false,
) )
sendEvent(VaultEvent.DismissPullToRefresh)
} }
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) { private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
@ -532,9 +534,9 @@ class VaultViewModel @Inject constructor(
vaultFilterType = vaultFilterTypeOrDefault, vaultFilterType = vaultFilterTypeOrDefault,
), ),
dialog = null, dialog = null,
isRefreshing = false,
) )
} }
sendEvent(VaultEvent.DismissPullToRefresh)
} }
private fun vaultLoadingReceive() { private fun vaultLoadingReceive() {
@ -551,8 +553,8 @@ class VaultViewModel @Inject constructor(
isIconLoadingDisabled = state.isIconLoadingDisabled, isIconLoadingDisabled = state.isIconLoadingDisabled,
hasMasterPassword = state.hasMasterPassword, hasMasterPassword = state.hasMasterPassword,
errorMessage = R.string.internet_connection_required_message.asText(), errorMessage = R.string.internet_connection_required_message.asText(),
isRefreshing = false,
) )
sendEvent(VaultEvent.DismissPullToRefresh)
} }
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) { private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
@ -633,6 +635,7 @@ data class VaultState(
val baseIconUrl: String, val baseIconUrl: String,
val isIconLoadingDisabled: Boolean, val isIconLoadingDisabled: Boolean,
val hideNotificationsDialog: Boolean, val hideNotificationsDialog: Boolean,
val isRefreshing: Boolean,
) : Parcelable { ) : Parcelable {
/** /**
@ -921,11 +924,6 @@ data class VaultState(
* Models effects for the [VaultScreen]. * Models effects for the [VaultScreen].
*/ */
sealed class VaultEvent { sealed class VaultEvent {
/**
* Dismisses the pull-to-refresh indicator.
*/
data object DismissPullToRefresh : VaultEvent()
/** /**
* Navigate to the Vault Search screen. * Navigate to the Vault Search screen.
*/ */
@ -1186,6 +1184,7 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
hasMasterPassword: Boolean, hasMasterPassword: Boolean,
errorTitle: Text, errorTitle: Text,
errorMessage: Text, errorMessage: Text,
isRefreshing: Boolean,
) { ) {
this.update { this.update {
if (vaultData != null) { if (vaultData != null) {
@ -1201,6 +1200,7 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
title = errorTitle, title = errorTitle,
message = errorMessage, message = errorMessage,
), ),
isRefreshing = isRefreshing,
) )
} else { } else {
it.copy( it.copy(
@ -1208,6 +1208,7 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
message = errorMessage, message = errorMessage,
), ),
dialog = null, dialog = null,
isRefreshing = isRefreshing,
) )
} }
} }

View file

@ -8,11 +8,11 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
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.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -60,7 +60,7 @@ fun VaultVerificationCodeItem(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary), indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onItemClick, onClick = onItemClick,
) )
.defaultMinSize(minHeight = 72.dp) .defaultMinSize(minHeight = 72.dp)

View file

@ -7,10 +7,8 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
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.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -31,6 +29,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderTextWithSupportLabel import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderTextWithSupportLabel
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.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.handlers.VerificationCodeHandlers import com.x8bit.bitwarden.ui.vault.feature.verificationcode.handlers.VerificationCodeHandlers
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -54,16 +53,16 @@ fun VerificationCodeScreen(
VerificationCodeHandlers.create(viewModel) VerificationCodeHandlers.create(viewModel)
} }
val pullToRefreshState = rememberPullToRefreshState().takeIf { state.isPullToRefreshEnabled } val pullToRefreshState = rememberBitwardenPullToRefreshState(
LaunchedEffect(key1 = pullToRefreshState?.isRefreshing) { isEnabled = state.isPullToRefreshEnabled,
if (pullToRefreshState?.isRefreshing == true) { isRefreshing = state.isRefreshing,
viewModel.trySendAction(VerificationCodeAction.RefreshPull) onRefresh = remember(viewModel) {
} { viewModel.trySendAction(VerificationCodeAction.RefreshPull) }
} },
)
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
is VerificationCodeEvent.DismissPullToRefresh -> pullToRefreshState?.endRefresh()
is VerificationCodeEvent.NavigateBack -> onNavigateBack() is VerificationCodeEvent.NavigateBack -> onNavigateBack()
is VerificationCodeEvent.NavigateToVaultItem -> onNavigateToVaultItemScreen(event.id) is VerificationCodeEvent.NavigateToVaultItem -> onNavigateToVaultItemScreen(event.id)
is VerificationCodeEvent.NavigateToVaultSearchScreen -> { is VerificationCodeEvent.NavigateToVaultSearchScreen -> {
@ -74,7 +73,6 @@ fun VerificationCodeScreen(
VerificationCodeDialogs(dialogState = state.dialogState) VerificationCodeDialogs(dialogState = state.dialogState)
@OptIn(ExperimentalMaterial3Api::class)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold( BitwardenScaffold(
modifier = Modifier modifier = Modifier

View file

@ -49,6 +49,7 @@ class VerificationCodeViewModel @Inject constructor(
vaultFilterType = vaultRepository.vaultFilterType, vaultFilterType = vaultRepository.vaultFilterType,
viewState = VerificationCodeState.ViewState.Loading, viewState = VerificationCodeState.ViewState.Loading,
dialogState = null, dialogState = null,
isRefreshing = false,
) )
}, },
) { ) {
@ -123,6 +124,7 @@ class VerificationCodeViewModel @Inject constructor(
} }
private fun handleRefreshPull() { private fun handleRefreshPull() {
mutableStateFlow.update { it.copy(isRefreshing = true) }
// The Pull-To-Refresh composable is already in the refreshing state. // The Pull-To-Refresh composable is already in the refreshing state.
// We will reset that state when sendDataStateFlow emits later on. // We will reset that state when sendDataStateFlow emits later on.
vaultRepository.sync() vaultRepository.sync()
@ -228,7 +230,7 @@ class VerificationCodeViewModel @Inject constructor(
) )
} }
} }
sendEvent(VerificationCodeEvent.DismissPullToRefresh) mutableStateFlow.update { it.copy(isRefreshing = false) }
} }
private fun vaultPendingReceive( private fun vaultPendingReceive(
@ -248,7 +250,7 @@ class VerificationCodeViewModel @Inject constructor(
verificationCodeData = verificationCodeData.data, verificationCodeData = verificationCodeData.data,
clearDialogState = true, clearDialogState = true,
) )
sendEvent(VerificationCodeEvent.DismissPullToRefresh) mutableStateFlow.update { it.copy(isRefreshing = false) }
} }
private fun vaultLoadingReceive() { private fun vaultLoadingReceive() {
@ -271,7 +273,7 @@ class VerificationCodeViewModel @Inject constructor(
) )
} }
} }
sendEvent(VerificationCodeEvent.DismissPullToRefresh) mutableStateFlow.update { it.copy(isRefreshing = false) }
} }
private fun updateStateWithVerificationCodeData( private fun updateStateWithVerificationCodeData(
@ -339,6 +341,7 @@ data class VerificationCodeState(
val baseIconUrl: String, val baseIconUrl: String,
val dialogState: DialogState?, val dialogState: DialogState?,
val isPullToRefreshSettingEnabled: Boolean, val isPullToRefreshSettingEnabled: Boolean,
val isRefreshing: Boolean,
) : Parcelable { ) : Parcelable {
/** /**
@ -421,12 +424,6 @@ data class VerificationCodeDisplayItem(
* Models events for the [VerificationCodeScreen]. * Models events for the [VerificationCodeScreen].
*/ */
sealed class VerificationCodeEvent { sealed class VerificationCodeEvent {
/**
* Dismisses the pull-to-refresh indicator.
*/
data object DismissPullToRefresh : VerificationCodeEvent()
/** /**
* Navigate back. * Navigate back.
*/ */

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
import android.net.Uri import android.net.Uri
import androidx.compose.ui.test.assert import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.isDialog
@ -124,12 +125,14 @@ class LoginWithDeviceScreenTest : BaseComposeTest() {
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = LoginWithDeviceState.ViewState.Loading) it.copy(viewState = LoginWithDeviceState.ViewState.Loading)
} }
composeTestRule.onNode(isProgressBar).assertIsDisplayed() // There are 2 because of the pull-to-refresh
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = DEFAULT_STATE.viewState) it.copy(viewState = DEFAULT_STATE.viewState)
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
} }
@Test @Test

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.platform.feature.search package com.x8bit.bitwarden.ui.platform.feature.search
import androidx.compose.ui.test.assert import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.filterToOne
@ -125,13 +126,15 @@ class SearchScreenTest : BaseComposeTest() {
@Test @Test
fun `progressbar should be displayed according to state`() { fun `progressbar should be displayed according to state`() {
mutableStateFlow.update { DEFAULT_STATE } mutableStateFlow.update { DEFAULT_STATE }
composeTestRule.onNode(isProgressBar).assertIsDisplayed() // There are 2 because of the pull-to-refresh
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = SearchState.ViewState.Empty(message = null)) it.copy(viewState = SearchState.ViewState.Empty(message = null))
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
} }
@Test @Test

View file

@ -119,6 +119,7 @@ class PendingRequestsScreenTest : BaseComposeTest() {
authRequests = emptyList(), authRequests = emptyList(),
viewState = PendingRequestsState.ViewState.Loading, viewState = PendingRequestsState.ViewState.Loading,
isPullToRefreshSettingEnabled = false, isPullToRefreshSettingEnabled = false,
isRefreshing = false,
) )
} }
} }

View file

@ -376,6 +376,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
authRequests = emptyList(), authRequests = emptyList(),
viewState = PendingRequestsState.ViewState.Empty, viewState = PendingRequestsState.ViewState.Empty,
isPullToRefreshSettingEnabled = false, isPullToRefreshSettingEnabled = false,
isRefreshing = false,
) )
} }
} }

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.tools.feature.send package com.x8bit.bitwarden.ui.tools.feature.send
import androidx.compose.ui.test.assert import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.filterToOne
@ -263,22 +264,26 @@ class SendScreenTest : BaseComposeTest() {
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = SendState.ViewState.Loading) it.copy(viewState = SendState.ViewState.Loading)
} }
composeTestRule.onNode(isProgressBar).assertIsDisplayed() // There are 2 because of the pull-to-refresh
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = SendState.ViewState.Empty) it.copy(viewState = SendState.ViewState.Empty)
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = SendState.ViewState.Error("Fail".asText())) it.copy(viewState = SendState.ViewState.Error("Fail".asText()))
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE) it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE)
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
} }
@Test @Test
@ -709,6 +714,7 @@ private val DEFAULT_STATE: SendState = SendState(
dialogState = null, dialogState = null,
isPullToRefreshSettingEnabled = false, isPullToRefreshSettingEnabled = false,
policyDisablesSend = false, policyDisablesSend = false,
isRefreshing = false,
) )
private val DEFAULT_SEND_ITEM: SendState.ViewState.Content.SendItem = private val DEFAULT_SEND_ITEM: SendState.ViewState.Content.SendItem =

View file

@ -313,16 +313,13 @@ class SendViewModelTest : BaseViewModelTest() {
assertEquals(initialState.copy(dialogState = null), viewModel.stateFlow.value) assertEquals(initialState.copy(dialogState = null), viewModel.stateFlow.value)
} }
@Suppress("MaxLineLength")
@Test @Test
fun `VaultRepository SendData Error should update view state to Error and emit DismissPullToRefresh`() = fun `VaultRepository SendData Error should update view state to Error`() = runTest {
runTest {
val dialogState = SendState.DialogState.Loading(R.string.syncing.asText()) val dialogState = SendState.DialogState.Loading(R.string.syncing.asText())
val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState))
viewModel.eventFlow.test { viewModel.eventFlow.test {
mutableSendDataFlow.value = DataState.Error(Throwable("Fail")) mutableSendDataFlow.value = DataState.Error(Throwable("Fail"))
assertEquals(SendEvent.DismissPullToRefresh, awaitItem())
} }
assertEquals( assertEquals(
@ -331,14 +328,14 @@ class SendViewModelTest : BaseViewModelTest() {
message = R.string.generic_error_message.asText(), message = R.string.generic_error_message.asText(),
), ),
dialogState = null, dialogState = null,
isRefreshing = false,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
} }
@Test @Test
fun `VaultRepository SendData Loaded should update view state and emit DismissPullToRefresh`() = fun `VaultRepository SendData Loaded should update view state`() = runTest {
runTest {
val dialogState = SendState.DialogState.Loading(R.string.syncing.asText()) val dialogState = SendState.DialogState.Loading(R.string.syncing.asText())
val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState))
val viewState = mockk<SendState.ViewState.Content>() val viewState = mockk<SendState.ViewState.Content>()
@ -350,11 +347,14 @@ class SendViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
mutableSendDataFlow.value = DataState.Loaded(sendData) mutableSendDataFlow.value = DataState.Loaded(sendData)
assertEquals(SendEvent.DismissPullToRefresh, awaitItem())
} }
assertEquals( assertEquals(
DEFAULT_STATE.copy(viewState = viewState, dialogState = null), DEFAULT_STATE.copy(
viewState = viewState,
dialogState = null,
isRefreshing = false,
),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
} }
@ -372,16 +372,13 @@ class SendViewModelTest : BaseViewModelTest() {
) )
} }
@Suppress("MaxLineLength")
@Test @Test
fun `VaultRepository SendData NoNetwork should update view state to Error and emit DismissPullToRefresh`() = fun `VaultRepository SendData NoNetwork should update view state to Error`() = runTest {
runTest {
val dialogState = SendState.DialogState.Loading(R.string.syncing.asText()) val dialogState = SendState.DialogState.Loading(R.string.syncing.asText())
val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState)) val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState))
viewModel.eventFlow.test { viewModel.eventFlow.test {
mutableSendDataFlow.value = DataState.NoNetwork() mutableSendDataFlow.value = DataState.NoNetwork()
assertEquals(SendEvent.DismissPullToRefresh, awaitItem())
} }
assertEquals( assertEquals(
@ -395,6 +392,7 @@ class SendViewModelTest : BaseViewModelTest() {
), ),
), ),
dialogState = null, dialogState = null,
isRefreshing = false,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
@ -470,4 +468,5 @@ private val DEFAULT_STATE: SendState = SendState(
dialogState = null, dialogState = null,
isPullToRefreshSettingEnabled = false, isPullToRefreshSettingEnabled = false,
policyDisablesSend = false, policyDisablesSend = false,
isRefreshing = false,
) )

View file

@ -1,6 +1,8 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.assert import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsNotDisplayed
@ -19,6 +21,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
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.performScrollTo
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
@ -409,7 +412,9 @@ class AddSendScreenTest : BaseComposeTest() {
fun `File segmented button click should send FileTypeClick`() { fun `File segmented button click should send FileTypeClick`() {
composeTestRule composeTestRule
.onNodeWithText("File") .onNodeWithText("File")
.performClick() // A bug prevents performClick from working here so we
// have to perform the semantic action instead.
.performSemanticsAction(SemanticsActions.OnClick)
verify { viewModel.trySendAction(AddSendAction.FileTypeClick) } verify { viewModel.trySendAction(AddSendAction.FileTypeClick) }
} }
@ -417,7 +422,9 @@ class AddSendScreenTest : BaseComposeTest() {
fun `Text segmented button click should send TextTypeClick`() { fun `Text segmented button click should send TextTypeClick`() {
composeTestRule composeTestRule
.onAllNodesWithText("Text")[0] .onAllNodesWithText("Text")[0]
.performClick() // A bug prevents performClick from working here so we
// have to perform the semantic action instead.
.performSemanticsAction(SemanticsActions.OnClick)
verify { viewModel.trySendAction(AddSendAction.TextTypeClick) } verify { viewModel.trySendAction(AddSendAction.TextTypeClick) }
} }
@ -912,17 +919,20 @@ class AddSendScreenTest : BaseComposeTest() {
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = AddSendState.ViewState.Loading) it.copy(viewState = AddSendState.ViewState.Loading)
} }
composeTestRule.onNode(isProgressBar).assertIsDisplayed() // There are 2 because of the pull-to-refresh
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = AddSendState.ViewState.Error("Fail".asText())) it.copy(viewState = AddSendState.ViewState.Error("Fail".asText()))
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = DEFAULT_VIEW_STATE) it.copy(viewState = DEFAULT_VIEW_STATE)
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
} }
@Test @Test

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.assert import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsNotDisplayed
@ -580,12 +581,14 @@ class VaultAddEditScreenTest : BaseComposeTest() {
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = VaultAddEditState.ViewState.Loading) it.copy(viewState = VaultAddEditState.ViewState.Loading)
} }
composeTestRule.onNode(isProgressBar).assertIsDisplayed() // There are 2 because of the pull-to-refresh
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = VaultAddEditState.ViewState.Error("Fail".asText())) it.copy(viewState = VaultAddEditState.ViewState.Error("Fail".asText()))
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
@ -596,7 +599,8 @@ class VaultAddEditScreenTest : BaseComposeTest() {
), ),
) )
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
} }
@Test @Test

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.attachments package com.x8bit.bitwarden.ui.vault.feature.attachments
import androidx.compose.ui.test.assert import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasAnyAncestor
@ -89,17 +90,20 @@ class AttachmentsScreenTest : BaseComposeTest() {
@Test @Test
fun `progressbar should be displayed according to state`() { fun `progressbar should be displayed according to state`() {
mutableStateFlow.update { it.copy(viewState = AttachmentsState.ViewState.Loading) } mutableStateFlow.update { it.copy(viewState = AttachmentsState.ViewState.Loading) }
composeTestRule.onNode(isProgressBar).assertIsDisplayed() // There are 2 because of the pull-to-refresh
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = AttachmentsState.ViewState.Error("Fail".asText())) it.copy(viewState = AttachmentsState.ViewState.Error("Fail".asText()))
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = DEFAULT_CONTENT_WITHOUT_ATTACHMENTS) it.copy(viewState = DEFAULT_CONTENT_WITHOUT_ATTACHMENTS)
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
} }
@Test @Test

View file

@ -22,7 +22,6 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onSiblings import androidx.compose.ui.test.onSiblings
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.core.net.toUri import androidx.core.net.toUri
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
@ -1364,9 +1363,8 @@ class VaultItemScreenTest : BaseComposeTest() {
) )
} }
composeTestRule // Only pull-to-refresh remains
.onNode(isProgressBar) composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
.assertDoesNotExist()
composeTestRule composeTestRule
.onNodeWithText("Passkey") .onNodeWithText("Passkey")
@ -1411,9 +1409,8 @@ class VaultItemScreenTest : BaseComposeTest() {
) )
} }
composeTestRule // Only pull-to-refresh remains
.onNode(isProgressBar) composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
.assertDoesNotExist()
composeTestRule composeTestRule
.onNodeWithContentDescription("Copy TOTP") .onNodeWithContentDescription("Copy TOTP")
@ -1434,10 +1431,8 @@ class VaultItemScreenTest : BaseComposeTest() {
) )
} }
composeTestRule // There are 2 because of the pull-to-refresh
.onNode(isProgressBar) composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2)
.performScrollTo()
.assertIsDisplayed()
composeTestRule composeTestRule
.onNodeWithContentDescription("Copy TOTP") .onNodeWithContentDescription("Copy TOTP")
@ -1452,10 +1447,8 @@ class VaultItemScreenTest : BaseComposeTest() {
) )
} }
composeTestRule // There are 2 because of the pull-to-refresh
.onNode(isProgressBar) composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2)
.performScrollTo()
.assertIsDisplayed()
composeTestRule composeTestRule
.onNodeWithContentDescription("Copy TOTP") .onNodeWithContentDescription("Copy TOTP")
@ -1471,9 +1464,8 @@ class VaultItemScreenTest : BaseComposeTest() {
) )
} }
composeTestRule // Only pull-to-refresh remains
.onNode(isProgressBar) composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
.assertIsNotDisplayed()
composeTestRule composeTestRule
.onNodeWithContentDescription("Copy TOTP") .onNodeWithContentDescription("Copy TOTP")
@ -1680,19 +1672,22 @@ class VaultItemScreenTest : BaseComposeTest() {
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = VaultItemState.ViewState.Loading) it.copy(viewState = VaultItemState.ViewState.Loading)
} }
composeTestRule.onNode(isProgressBar).assertIsDisplayed() // There are 2 because of the pull-to-refresh
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2)
mutableStateFlow.update { mutableStateFlow.update {
it.copy(viewState = VaultItemState.ViewState.Error("Fail".asText())) it.copy(viewState = VaultItemState.ViewState.Error("Fail".asText()))
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
mutableStateFlow.update { currentState -> mutableStateFlow.update { currentState ->
updateLoginType(currentState) { updateLoginType(currentState) {
copy(totpCodeItemData = null) copy(totpCodeItemData = null)
} }
} }
composeTestRule.onNode(isProgressBar).assertDoesNotExist() // Only pull-to-refresh remains
composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
} }
@Test @Test

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import androidx.compose.ui.test.assert import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.assertTextEquals
@ -490,9 +491,8 @@ class VaultItemListingScreenTest : BaseComposeTest() {
fun `progressbar should be displayed according to state`() { fun `progressbar should be displayed according to state`() {
mutableStateFlow.update { DEFAULT_STATE } mutableStateFlow.update { DEFAULT_STATE }
composeTestRule // There are 2 because of the pull-to-refresh
.onNode(isProgressBar) composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2)
.assertIsDisplayed()
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
@ -504,9 +504,8 @@ class VaultItemListingScreenTest : BaseComposeTest() {
) )
} }
composeTestRule // Only pull-to-refresh remains
.onNode(isProgressBar) composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1)
.assertDoesNotExist()
} }
@Test @Test
@ -2059,6 +2058,7 @@ private val DEFAULT_STATE = VaultItemListingState(
policyDisablesSend = false, policyDisablesSend = false,
hasMasterPassword = true, hasMasterPassword = true,
isPremium = false, isPremium = false,
isRefreshing = false,
) )
private val STATE_FOR_AUTOFILL = DEFAULT_STATE.copy( private val STATE_FOR_AUTOFILL = DEFAULT_STATE.copy(

View file

@ -498,10 +498,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
VaultItemListingState.DialogState.Loading(R.string.saving.asText()), VaultItemListingState.DialogState.Loading(R.string.saving.asText()),
viewModel.stateFlow.value.dialogState, viewModel.stateFlow.value.dialogState,
) )
assertEquals(
VaultItemListingEvent.DismissPullToRefresh,
awaitItem(),
)
assertEquals( assertEquals(
VaultItemListingEvent.Fido2UserVerification( VaultItemListingEvent.Fido2UserVerification(
isRequired = true, isRequired = true,
@ -1195,8 +1191,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() = fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() = runTest {
runTest {
setupMockUri() setupMockUri()
val dataState = DataState.Loaded( val dataState = DataState.Loaded(
@ -1210,10 +1205,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
@ -1475,10 +1467,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
), ),
) )
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems( viewState = VaultItemListingState.ViewState.NoItems(
@ -1504,10 +1495,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems( viewState = VaultItemListingState.ViewState.NoItems(
@ -1626,10 +1615,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
viewState = VaultItemListingState.ViewState.Error( viewState = VaultItemListingState.ViewState.Error(
@ -1656,10 +1643,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
viewState = VaultItemListingState.ViewState.Content( viewState = VaultItemListingState.ViewState.Content(
@ -1691,10 +1676,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems( viewState = VaultItemListingState.ViewState.NoItems(
@ -1721,10 +1704,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems( viewState = VaultItemListingState.ViewState.NoItems(
@ -1743,10 +1724,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
viewState = VaultItemListingState.ViewState.Error( viewState = VaultItemListingState.ViewState.Error(
@ -1777,10 +1756,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
viewState = VaultItemListingState.ViewState.Content( viewState = VaultItemListingState.ViewState.Content(
@ -1811,10 +1788,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems( viewState = VaultItemListingState.ViewState.NoItems(
@ -1840,10 +1815,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(value = dataState) mutableVaultDataStateFlow.tryEmit(value = dataState)
assertEquals(VaultItemListingEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVaultItemListingState( createVaultItemListingState(
viewState = VaultItemListingState.ViewState.NoItems( viewState = VaultItemListingState.ViewState.NoItems(
@ -3763,6 +3736,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
hasMasterPassword = true, hasMasterPassword = true,
fido2CredentialRequest = null, fido2CredentialRequest = null,
isPremium = true, isPremium = true,
isRefreshing = false,
) )
} }

View file

@ -1190,6 +1190,7 @@ private val DEFAULT_STATE: VaultState = VaultState(
isIconLoadingDisabled = false, isIconLoadingDisabled = false,
hasMasterPassword = true, hasMasterPassword = true,
hideNotificationsDialog = true, hideNotificationsDialog = true,
isRefreshing = false,
) )
private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content( private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content(

View file

@ -612,7 +612,6 @@ class VaultViewModelTest : BaseViewModelTest() {
VaultEvent.ShowToast(R.string.syncing_complete.asText()), VaultEvent.ShowToast(R.string.syncing_complete.asText()),
awaitItem(), awaitItem(),
) )
assertEquals(VaultEvent.DismissPullToRefresh, awaitItem())
} }
} }
@ -660,7 +659,6 @@ class VaultViewModelTest : BaseViewModelTest() {
VaultEvent.ShowToast(R.string.syncing_complete.asText()), VaultEvent.ShowToast(R.string.syncing_complete.asText()),
awaitItem(), awaitItem(),
) )
assertEquals(VaultEvent.DismissPullToRefresh, awaitItem())
} }
} }
@ -1591,4 +1589,5 @@ private fun createMockVaultState(
isIconLoadingDisabled = false, isIconLoadingDisabled = false,
hasMasterPassword = true, hasMasterPassword = true,
hideNotificationsDialog = true, hideNotificationsDialog = true,
isRefreshing = false,
) )

View file

@ -407,4 +407,5 @@ private val DEFAULT_STATE = VerificationCodeState(
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isPullToRefreshSettingEnabled = false, isPullToRefreshSettingEnabled = false,
dialogState = null, dialogState = null,
isRefreshing = false,
) )

View file

@ -171,7 +171,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test @Test
fun `AuthCodeFlow Pending with data should update state to Content`() { fun `AuthCodeFlow Pending with data should update state to Content`() {
setupMockUri() setupMockUri()
@ -197,7 +196,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
) )
} }
@Suppress("MaxLineLength")
@Test @Test
fun `AuthCodeFlow Pending with no data should call NavigateBack to go to the vault screen`() = fun `AuthCodeFlow Pending with no data should call NavigateBack to go to the vault screen`() =
runTest { runTest {
@ -216,7 +214,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test @Test
fun `AuthCodeFlow Error with data should update state to Content`() = runTest { fun `AuthCodeFlow Error with data should update state to Content`() = runTest {
setupMockUri() setupMockUri()
@ -233,10 +230,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
), ),
) )
viewModel.eventFlow.test {
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVerificationCodeState( createVerificationCodeState(
viewState = VerificationCodeState.ViewState.Content( viewState = VerificationCodeState.ViewState.Content(
@ -247,7 +240,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
) )
} }
@Suppress("MaxLineLength")
@Test @Test
fun `AuthCodeFlow Error with no data should call NavigateBack to go to the vault screen`() = fun `AuthCodeFlow Error with no data should call NavigateBack to go to the vault screen`() =
runTest { runTest {
@ -264,11 +256,9 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem()) assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
} }
} }
@Suppress("MaxLineLength")
@Test @Test
fun `AuthCodeFlow Error with null data should show error screen`() = runTest { fun `AuthCodeFlow Error with null data should show error screen`() = runTest {
setupMockUri() setupMockUri()
@ -282,10 +272,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
), ),
) )
viewModel.eventFlow.test {
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVerificationCodeState( createVerificationCodeState(
viewState = VerificationCodeState.ViewState.Error( viewState = VerificationCodeState.ViewState.Error(
@ -308,7 +294,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem()) assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
} }
} }
@ -350,10 +335,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
), ),
) )
viewModel.eventFlow.test {
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVerificationCodeState( createVerificationCodeState(
viewState = VerificationCodeState.ViewState.Content( viewState = VerificationCodeState.ViewState.Content(
@ -375,7 +356,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem()) assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
} }
} }
@ -394,10 +374,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
), ),
) )
viewModel.eventFlow.test {
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
}
assertEquals( assertEquals(
createVerificationCodeState( createVerificationCodeState(
viewState = VerificationCodeState.ViewState.Content( viewState = VerificationCodeState.ViewState.Content(
@ -426,7 +402,6 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem()) assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
} }
} }
@ -527,6 +502,7 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl, baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
dialogState = null, dialogState = null,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value, isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
isRefreshing = false,
) )
private fun createDisplayItemList() = listOf( private fun createDisplayItemList() = listOf(

View file

@ -12,7 +12,7 @@ androidxActivity = "1.9.1"
androidXBiometrics = "1.2.0-alpha05" androidXBiometrics = "1.2.0-alpha05"
androidxBrowser = "1.8.0" androidxBrowser = "1.8.0"
androidxCamera = "1.3.4" androidxCamera = "1.3.4"
androidxComposeBom = "2024.08.00" androidxComposeBom = "2024.09.00"
androidxCore = "1.13.1" androidxCore = "1.13.1"
androidxCredentials = "1.2.2" androidxCredentials = "1.2.2"
androidxHiltNavigationCompose = "1.2.0" androidxHiltNavigationCompose = "1.2.0"