Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-15 17:32:29 +08:00
parent 207a2b52df
commit 50af6db261
20 changed files with 418 additions and 147 deletions

View file

@ -85,7 +85,7 @@ private fun BaseContainer(
Surface(
modifier = modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
.wrapContentWidth(),
.fillMaxWidth(),
shape = TangemTheme.shapes.roundedCornersXMedium,
color = containerColor,
) {

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.extensions
import android.content.res.Resources
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
@ -121,6 +122,22 @@ fun TextReference.resolveReference(): String {
}
}
/** Resolve [TextReference] as [String] using [resources] (non-composable context) */
fun TextReference.resolveReference(resources: Resources): String {
return when (this) {
is TextReference.Res -> resources.getString(id, *formatArgs.toTypedArray())
is TextReference.PluralRes -> resources.getQuantityString(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Combined -> {
buildString {
refs.forEach {
append(it.resolveReference(resources))
}
}
}
}
}
/** Concatenate [this] reference with [ref] */
operator fun TextReference.plus(ref: TextReference): TextReference {
return when (this) {

View file

@ -5,5 +5,7 @@ package com.tangem.domain.wallets.models
*/
sealed interface SaveWalletError {
object CommonError : SaveWalletError
object DataError : SaveWalletError
data class WalletAlreadySaved(val messageId: Int) : SaveWalletError
}

View file

@ -5,6 +5,7 @@ import arrow.core.left
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.models.SaveWalletError
import com.tangem.domain.wallets.models.UserWallet
@ -19,9 +20,17 @@ import com.tangem.domain.wallets.models.UserWallet
class SaveWalletUseCase(private val walletsStateHolder: WalletsStateHolder) {
suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either<SaveWalletError, Unit> {
requireNotNull(walletsStateHolder.userWalletsListManager).save(userWallet, canOverride)
val userWalletListManager = walletsStateHolder.userWalletsListManager
?: return SaveWalletError.DataError.left()
userWalletListManager.save(userWallet, canOverride)
.doOnSuccess { return Unit.right() }
.doOnFailure { return SaveWalletError.CommonError.left() }
.doOnFailure {
return when (it) {
is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved(it.messageResId)
else -> SaveWalletError.DataError
}.left()
}
return Unit.right()
}

View file

@ -383,6 +383,7 @@ internal object WalletPreviewData {
bottomSheetConfig = bottomSheet,
tokenActionsBottomSheet = actionsBottomSheet,
onManageTokensClick = {},
event = consumedEvent(),
)
}
@ -432,6 +433,7 @@ internal object WalletPreviewData {
),
),
),
event = consumedEvent(),
)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.feature.wallet.presentation.wallet.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class WalletEvent {
data class ChangeWallet(val index: Int) : WalletEvent()
data class ShowError(val text: TextReference) : WalletEvent()
}

View file

@ -1,6 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -23,6 +25,7 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() {
override val notifications: ImmutableList<WalletNotification>,
override val bottomSheetConfig: TangemBottomSheetConfig?,
override val tokensListState: WalletTokensListState,
override val event: StateEvent<WalletEvent> = consumedEvent(),
val tokenActionsBottomSheet: ActionsBottomSheetConfig?,
val onManageTokensClick: () -> Unit,
) : WalletMultiCurrencyState()
@ -37,6 +40,7 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() {
override val onScanClick: () -> Unit,
override val isBottomSheetShow: Boolean = false,
override val onBottomSheetDismiss: () -> Unit = {},
override val event: StateEvent<WalletEvent> = consumedEvent(),
) : WalletMultiCurrencyState(), WalletLockedState {
override val notifications = persistentListOf(

View file

@ -6,6 +6,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
@ -35,6 +37,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
override val bottomSheetConfig: TangemBottomSheetConfig?,
override val buttons: PersistentList<WalletManageButton>,
override val txHistoryState: TxHistoryState,
override val event: StateEvent<WalletEvent> = consumedEvent(),
val marketPriceBlockState: MarketPriceBlockState,
) : WalletSingleCurrencyState()
@ -49,6 +52,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
override val onScanClick: () -> Unit,
override val isBottomSheetShow: Boolean = false,
override val onBottomSheetDismiss: () -> Unit = {},
override val event: StateEvent<WalletEvent> = consumedEvent(),
val onExploreClick: () -> Unit,
) : WalletSingleCurrencyState(), WalletLockedState {

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.event.StateEvent
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import kotlinx.collections.immutable.ImmutableList
@ -32,28 +33,49 @@ internal sealed class WalletState {
/** Bottom sheet config */
abstract val bottomSheetConfig: TangemBottomSheetConfig?
/** State event */
abstract val event: StateEvent<WalletEvent>
/**
* Util function that allow to make a copy
*
* @param walletsListConfig wallets list config
* @param pullToRefreshConfig pull to refresh config
* @param event state event
*/
fun copySealed(
walletsListConfig: WalletsListConfig = this.walletsListConfig,
pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig,
event: StateEvent<WalletEvent> = this.event,
): ContentState {
return when (this) {
is WalletMultiCurrencyState.Content -> {
copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig)
copy(
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
event = event,
)
}
is WalletMultiCurrencyState.Locked -> {
copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig)
copy(
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
event = event,
)
}
is WalletSingleCurrencyState.Content -> {
copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig)
copy(
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
event = event,
)
}
is WalletSingleCurrencyState.Locked -> {
copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig)
copy(
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
event = event,
)
}
}
}

View file

@ -5,6 +5,8 @@ import arrow.core.Either
import com.tangem.common.Provider
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.tokens.error.CurrencyStatusError
@ -16,10 +18,7 @@ import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.*
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter
import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadingTxHistoryConverter
@ -36,8 +35,10 @@ import kotlinx.coroutines.flow.Flow
* @property currentStateProvider current ui state provider
* @property currentCardTypeResolverProvider current card type resolver
* @property currentWalletProvider current wallet
* @property appCurrencyProvider app currency provider
* @property clickIntents screen click intents
*/
@Suppress("TooManyFunctions")
internal class WalletStateFactory(
private val currentStateProvider: Provider<WalletState>,
private val currentCardTypeResolverProvider: Provider<CardTypesResolver>,
@ -246,4 +247,25 @@ internal class WalletStateFactory(
fun getStateByCurrencyStatusError(error: CurrencyStatusError): WalletState {
return currencyStatusErrorConverter.convert(error)
}
fun getStateAndTriggerEvent(
state: WalletState,
event: WalletEvent,
setUiState: (WalletState) -> Unit,
): WalletState {
return when (state) {
is WalletState.ContentState -> state.copySealed(
event = triggeredEvent(
data = event,
onConsume = {
val currentState = currentStateProvider()
if (currentState is WalletState.ContentState) {
setUiState(currentState.copySealed(event = consumedEvent()))
}
},
),
)
is WalletState.Initial -> state
}
}
}

View file

@ -63,7 +63,9 @@ internal class WalletLoadedTxHistoryConverter(
private fun convert(items: Flow<PagingData<TxHistoryItem>>): WalletState {
return when (val state = currentStateProvider()) {
is WalletSingleCurrencyState.Content -> {
state.copy(txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items))
return state.copy(
txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items) ?: state.txHistoryState,
)
}
is WalletMultiCurrencyState.Content,
is WalletMultiCurrencyState.Locked,

View file

@ -43,7 +43,7 @@ internal class WalletTxHistoryItemFlowConverter(
private val currentStateProvider: Provider<WalletState>,
private val blockchain: Blockchain,
private val clickIntents: WalletClickIntents,
) : Converter<Flow<PagingData<TxHistoryItem>>, TxHistoryState> {
) : Converter<Flow<PagingData<TxHistoryItem>>, TxHistoryState?> {
/** Example, 2 Aug, 2023 */
private val dateFormatter by lazy {
@ -67,9 +67,9 @@ internal class WalletTxHistoryItemFlowConverter(
.withLocale(Locale.getDefault())
}
override fun convert(value: Flow<PagingData<TxHistoryItem>>): TxHistoryState {
val state = currentStateProvider() as WalletSingleCurrencyState
val txHistoryContent = state.txHistoryState as TxHistoryState.Content
override fun convert(value: Flow<PagingData<TxHistoryItem>>): TxHistoryState? {
val state = currentStateProvider() as? WalletSingleCurrencyState ?: return null
val txHistoryContent = state.txHistoryState as? TxHistoryState.Content ?: return state.txHistoryState
// FIXME: TxHistoryRepository should send loading transactions
// [REDACTED_JIRA]

View file

@ -0,0 +1,36 @@
package com.tangem.feature.wallet.presentation.wallet.ui
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent
import kotlinx.coroutines.delay
@Composable
internal fun WalletEventEffect(
walletsListState: LazyListState,
snackbarHostState: SnackbarHostState,
event: StateEvent<WalletEvent>,
onAutoScrollSet: () -> Unit,
) {
val resources = LocalContext.current.resources
EventEffect(
event = event,
onTrigger = { value ->
when (value) {
is WalletEvent.ChangeWallet -> {
onAutoScrollSet()
delay(timeMillis = 800)
walletsListState.animateScrollToItem(index = value.index)
}
is WalletEvent.ShowError -> {
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources))
}
}
},
)
}

View file

@ -3,13 +3,13 @@ package com.tangem.feature.wallet.presentation.wallet.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -28,6 +28,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencySt
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState
import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet
@ -50,27 +51,44 @@ internal fun WalletScreen(state: WalletState) {
BackHandler(onBack = state.onBackClick)
when (state) {
is WalletState.ContentState -> WalletContent(state = state)
is WalletState.ContentState -> {
val walletsListState = rememberLazyListState()
val snackbarHostState = remember { SnackbarHostState() }
val isAutoScroll = remember { mutableStateOf(value = false) }
WalletContent(
state = state,
walletsListState = walletsListState,
snackbarHostState = snackbarHostState,
isAutoScroll = isAutoScroll,
onAutoScrollReset = { isAutoScroll.value = false },
)
WalletEventEffect(
walletsListState = walletsListState,
snackbarHostState = snackbarHostState,
event = state.event,
onAutoScrollSet = { isAutoScroll.value = true },
)
}
is WalletState.Initial -> Unit
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun WalletContent(state: WalletState.ContentState) {
val walletsListState = rememberLazyListState()
BaseScaffold(state = state) { scaffoldPaddings ->
private fun WalletContent(
state: WalletState.ContentState,
walletsListState: LazyListState,
snackbarHostState: SnackbarHostState,
isAutoScroll: State<Boolean>,
onAutoScrollReset: () -> Unit,
) {
BaseScaffold(state = state, snackbarHostState) { scaffoldPaddings ->
val movableItemModifier = Modifier.changeWalletAnimator(walletsListState)
val pullRefreshState = rememberPullRefreshState(
refreshing = state.pullToRefreshConfig.isRefreshing,
onRefresh = state.pullToRefreshConfig.onRefresh,
)
Box(
modifier = Modifier
.padding(paddingValues = scaffoldPaddings)
.pullRefresh(pullRefreshState),
UpdatableContainer(
pullToRefreshConfig = state.pullToRefreshConfig,
modifier = Modifier.padding(paddingValues = scaffoldPaddings),
) {
val txHistoryItems = if (state is WalletSingleCurrencyState &&
state.txHistoryState is TxHistoryState.Content
@ -126,24 +144,51 @@ private fun WalletContent(state: WalletState.ContentState) {
}
}
}
WalletPullToRefreshIndicator(
isRefreshing = state.pullToRefreshConfig.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}
WalletBottomSheets(state = state)
WalletSideEffects(lazyListState = walletsListState, walletsListConfig = state.walletsListConfig)
WalletsListEffects(
lazyListState = walletsListState,
walletsListConfig = state.walletsListConfig,
isAutoScroll = isAutoScroll,
onAutoScrollReset = onAutoScrollReset,
)
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun UpdatableContainer(
pullToRefreshConfig: WalletPullToRefreshConfig,
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
) {
val pullRefreshState = rememberPullRefreshState(
refreshing = pullToRefreshConfig.isRefreshing,
onRefresh = pullToRefreshConfig.onRefresh,
)
Box(modifier = modifier.pullRefresh(pullRefreshState)) {
content()
WalletPullToRefreshIndicator(
isRefreshing = pullToRefreshConfig.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}
@Composable
private fun BaseScaffold(state: WalletState.ContentState, content: @Composable (PaddingValues) -> Unit) {
private fun BaseScaffold(
state: WalletState.ContentState,
snackbarHostState: SnackbarHostState,
content: @Composable (PaddingValues) -> Unit,
) {
Scaffold(
topBar = { WalletTopBar(config = state.topBarConfig) },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
if (state is WalletMultiCurrencyState.Content) {
ManageTokensButton(onManageTokensClick = state.onManageTokensClick)
@ -193,7 +238,7 @@ private fun WalletBottomSheets(state: WalletState) {
@Composable
private fun WalletScreenPreview_Light(@PreviewParameter(WalletScreenParameterProvider::class) state: WalletState) {
TangemTheme {
WalletScreen(state)
WalletScreen(state = state)
}
}
@ -201,7 +246,7 @@ private fun WalletScreenPreview_Light(@PreviewParameter(WalletScreenParameterPro
@Composable
private fun WalletScreenPreview_Dark(@PreviewParameter(WalletScreenParameterProvider::class) state: WalletState) {
TangemTheme(isDark = true) {
WalletScreen(state)
WalletScreen(state = state)
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.feature.wallet.presentation.wallet.ui
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.snapshotFlow
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector
import com.tangem.feature.wallet.presentation.wallet.ui.utils.WalletsListInteractionsCollector
@Composable
internal fun WalletsListEffects(
lazyListState: LazyListState,
walletsListConfig: WalletsListConfig,
isAutoScroll: State<Boolean>,
onAutoScrollReset: () -> Unit,
) {
LaunchedEffect(key1 = lazyListState, key2 = walletsListConfig.onWalletChange) {
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
.collect(
collector = ScrollOffsetCollector(
lazyListState = lazyListState,
walletsListConfig = walletsListConfig,
isAutoScroll = isAutoScroll,
),
)
}
LaunchedEffect(Unit) {
lazyListState.interactionSource.interactions.collect(
collector = WalletsListInteractionsCollector(onDragStart = onAutoScrollReset),
)
}
}

View file

@ -1,7 +1,11 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import androidx.compose.animation.core.*
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
@ -10,19 +14,20 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard
private const val SHORT_SNAP_ELEMENT_COUNT = 50
/**
* Wallets list component
*
@ -43,7 +48,7 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState
state = lazyListState,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState, itemWidth = itemWidth),
) {
items(
items = config.wallets,
@ -60,6 +65,35 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState
}
}
/**
* Custom implementation of fling behaviour that overrides 'shortSnapVelocityThreshold'.
* Every user's drag action will similar to a short snap
* if drag offset is less than [SHORT_SNAP_ELEMENT_COUNT] * item width.
*
* @param lazyListState lazy list state
* @param itemWidth list item width
*
* @see rememberSnapFlingBehavior
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState, itemWidth: Dp): SnapFlingBehavior {
val snappingLayout = remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) }
val density = LocalDensity.current
val highVelocityApproachSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
return remember(key1 = snappingLayout, key2 = highVelocityApproachSpec, key3 = density) {
SnapFlingBehavior(
snapLayoutInfoProvider = snappingLayout,
lowVelocityAnimationSpec = tween(durationMillis = 1000, easing = LinearEasing),
highVelocityAnimationSpec = highVelocityApproachSpec,
snapAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow),
density = density,
shortSnapVelocityThreshold = itemWidth * SHORT_SNAP_ELEMENT_COUNT,
)
}
}
@Preview
@Composable
private fun Preview_WalletsList_LightTheme() {

View file

@ -1,38 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.snapshotFlow
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector
/**
* Wallet screen side effects
*
* @param lazyListState lazy list state
* @param walletsListConfig wallets list config
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun WalletSideEffects(lazyListState: LazyListState, walletsListConfig: WalletsListConfig) {
LaunchedEffect(key1 = walletsListConfig.selectedWalletIndex) {
if (!lazyListState.isScrollInProgress) {
lazyListState.animateScrollToItem(walletsListConfig.selectedWalletIndex)
}
}
val dragInteraction = lazyListState.interactionSource.interactions.collectAsState(initial = null)
LaunchedEffect(key1 = lazyListState, key2 = walletsListConfig.onWalletChange) {
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
.collect(
collector = ScrollOffsetCollector(
lazyListState = lazyListState,
dragInteraction = dragInteraction,
callback = walletsListConfig.onWalletChange,
),
)
}
}

View file

@ -1,50 +1,58 @@
package com.tangem.feature.wallet.presentation.wallet.ui.utils
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.State
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
import kotlinx.coroutines.flow.FlowCollector
import kotlin.math.abs
/**
* Flow collector for scroll items tracking.
* If first visible item offset is greater than half item size, then [callback] be invoked.
* If last visible item offset is greater than half item size, then [callback] be invoked.
* If first visible item offset is greater than half item size, then change selected wallet index.
* If last visible item offset is greater than half item size, then change selected wallet index.
*
* @property lazyListState lazy list state
* @property dragInteraction current drag interaction
* @property callback lambda be invoked when current scroll items is changed
* @property lazyListState lazy list state
* @property walletsListConfig wallets list config
* @property isAutoScroll check if last scrolling is auto scroll
*
[REDACTED_AUTHOR]
*/
internal class ScrollOffsetCollector(
private val lazyListState: LazyListState,
private val dragInteraction: State<Interaction?>,
private val callback: (Int) -> Unit,
private val walletsListConfig: WalletsListConfig,
private val isAutoScroll: State<Boolean>,
) : FlowCollector<List<LazyListItemInfo>> {
private val LazyListItemInfo.halfItemSize get() = size.div(other = 2)
private var currentIndex = walletsListConfig.selectedWalletIndex
set(value) {
if (field != value) {
field = value
}
}
override suspend fun emit(value: List<LazyListItemInfo>) {
if (isNotUserInteraction() || value.size <= 1) return
// Auto scroll must not change wallet
if (isAutoScroll.value) {
currentIndex = walletsListConfig.selectedWalletIndex
return
}
if (!lazyListState.isScrollInProgress || value.size <= 1) return
val firstItem = value.firstOrNull() ?: return
val lastItem = value.lastOrNull() ?: return
if (abs(firstItem.offset) > firstItem.halfItemSize) {
callback(firstItem.index + 1)
val newIndex = firstItem.index + 1
currentIndex = newIndex
walletsListConfig.onWalletChange(newIndex)
} else if (abs(lastItem.offset) > lastItem.halfItemSize) {
callback(lastItem.index - 1)
val newIndex = lastItem.index - 1
currentIndex = newIndex
walletsListConfig.onWalletChange(newIndex)
}
}
/**
* Sometimes the list is scrolled programmatically. Example: selecting a specific wallet when a user opens the
* screen for the first time or scans a new wallet. Therefore [ScrollOffsetCollector] should not respond to changes.
*/
private fun isNotUserInteraction(): Boolean {
return !lazyListState.isScrollInProgress || dragInteraction.value !is DragInteraction.Start
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.feature.wallet.presentation.wallet.ui.utils
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.Interaction
import kotlinx.coroutines.flow.FlowCollector
internal class WalletsListInteractionsCollector(
private val onDragStart: () -> Unit,
) : FlowCollector<Interaction?> {
override suspend fun emit(value: Interaction?) {
if (value is DragInteraction.Start) onDragStart()
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.*
@ -30,15 +31,13 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.SaveWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.state.WalletLockedState
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.*
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
@ -51,6 +50,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.properties.Delegates
@ -131,6 +131,7 @@ internal class WalletViewModel @Inject constructor(
private val buttonsJobHolder = JobHolder()
private val notificationsJobHolder = JobHolder()
private val refreshContentJobHolder = JobHolder()
private val onWalletChangeJobHolder = JobHolder()
private val walletsUpdateActionResolver = WalletsUpdateActionResolver(
currentStateProvider = Provider { uiState },
@ -181,18 +182,17 @@ internal class WalletViewModel @Inject constructor(
}
}
private fun loadAndUpdateState(index: Int) {
uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index)
getContentItemsUpdates(index = index)
}
private fun deleteWalletAndUpdateState(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
val cacheState = WalletStateCache.getState(userWalletId = action.selectedWalletId)
if (cacheState != null) {
uiState = stateFactory.getStateWithoutDeletedWallet(cacheState, action)
if (cacheState.isLoadingState()) {
uiState = stateFactory.getStateAndTriggerEvent(
state = uiState,
event = WalletEvent.ChangeWallet(action.selectedWalletIndex),
setUiState = { uiState = it },
)
getContentItemsUpdates(action.selectedWalletIndex)
}
} else {
@ -200,6 +200,18 @@ internal class WalletViewModel @Inject constructor(
}
}
private fun loadAndUpdateState(index: Int) {
uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index)
uiState = stateFactory.getStateAndTriggerEvent(
state = uiState,
event = WalletEvent.ChangeWallet(index = index),
setUiState = { uiState = it },
)
getContentItemsUpdates(index = index)
}
override fun onBackClick() {
viewModelScope.launch(dispatchers.main) {
router.popBackStack(screen = if (shouldSaveUserWalletsUseCase()) AppScreen.Welcome else AppScreen.Home)
@ -215,8 +227,31 @@ internal class WalletViewModel @Inject constructor(
if (userWallet != null) {
saveWalletUseCase(userWallet = userWallet, canOverride = false)
.onLeft { saveWalletError ->
when (saveWalletError) {
is SaveWalletError.DataError -> Unit
is SaveWalletError.WalletAlreadySaved -> {
uiState = stateFactory.getStateAndTriggerEvent(
state = uiState,
event = WalletEvent.ShowError(
text = TextReference.Res(saveWalletError.messageId),
),
setUiState = { uiState = it },
)
}
}
}
}
}
.doOnFailure { tangemError ->
uiState = stateFactory.getStateAndTriggerEvent(
state = uiState,
event = WalletEvent.ShowError(
text = TextReference.Str(tangemError.customMessage),
),
setUiState = { uiState = it },
)
}
}
}
@ -307,39 +342,45 @@ internal class WalletViewModel @Inject constructor(
val state = uiState as? WalletState.ContentState ?: return
if (state.walletsListConfig.selectedWalletIndex == index) return
viewModelScope.launch(dispatchers.io) {
selectWalletUseCase(getWallet(index = index).walletId)
}
// Reset the job to avoid a redundant state updating
onWalletChangeJobHolder.update(null)
val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id)
if (cacheState != null && cacheState !is WalletLockedState) {
uiState = cacheState.copySealed(
walletsListConfig = state.walletsListConfig.copy(
selectedWalletIndex = index,
wallets = state.walletsListConfig.wallets
.mapIndexed { mapIndex, currentWallet ->
val cacheWallet = cacheState.walletsListConfig.wallets.getOrNull(mapIndex)
if (currentWallet is WalletCardState.Loading && cacheWallet != null &&
cacheWallet.isLoaded()
) {
cacheWallet
} else {
currentWallet
}
}
.toImmutableList(),
),
pullToRefreshConfig = cacheState.pullToRefreshConfig.copy(isRefreshing = false),
)
if (cacheState.isLoadingState()) {
getContentItemsUpdates(index)
viewModelScope.launch(dispatchers.main) {
withContext(dispatchers.io) {
selectWalletUseCase(userWalletId = state.walletsListConfig.wallets[index].id)
}
val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id)
if (cacheState != null && cacheState !is WalletLockedState) {
uiState = cacheState.copySealed(
walletsListConfig = state.walletsListConfig.copy(
selectedWalletIndex = index,
wallets = state.walletsListConfig.wallets
.mapIndexed { mapIndex, currentWallet ->
val cacheWallet = cacheState.walletsListConfig.wallets.getOrNull(mapIndex)
if (currentWallet is WalletCardState.Loading && cacheWallet != null &&
cacheWallet.isLoaded()
) {
cacheWallet
} else {
currentWallet
}
}
.toImmutableList(),
),
pullToRefreshConfig = cacheState.pullToRefreshConfig.copy(isRefreshing = false),
)
if (cacheState.isLoadingState()) {
getContentItemsUpdates(index)
}
} else {
uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index)
getContentItemsUpdates(index = index)
}
} else {
uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index)
getContentItemsUpdates(index = index)
}
.saveIn(onWalletChangeJobHolder)
}
private fun WalletCardState.isLoaded(): Boolean {