Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-27 12:27:10 +08:00
parent 31f513725f
commit d2d7dd6e71
17 changed files with 900 additions and 1 deletions

View file

@ -17,4 +17,16 @@ internal sealed class WalletEvent {
data class CopyAddress(val address: String, val toast: TextReference) : WalletEvent()
data class RateApp(val onDismissClick: () -> Unit) : WalletEvent()
data class DemonstrateWalletsScrollPreview(val direction: Direction) : WalletEvent() {
enum class Direction {
/** 1 -> 2 */
LEFT,
/** 1 <- 2 */
RIGHT,
}
}
}

View file

@ -0,0 +1,187 @@
package com.tangem.feature.wallet.presentation.wallet.state2
import androidx.paging.PagingData
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.extensions.TextReference
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.components.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import javax.annotation.concurrent.Immutable
const val NOT_INITIALIZED_WALLET_INDEX = -1
internal data class WalletScreenState(
val onBackClick: () -> Unit,
val topBarConfig: WalletTopBarConfig,
val selectedWalletIndex: Int,
val wallets: ImmutableList<WalletState>,
val onWalletChange: (Int) -> Unit,
val event: StateEvent<WalletEvent>,
val isHidingMode: Boolean,
)
internal sealed class WalletState {
abstract val pullToRefreshConfig: WalletPullToRefreshConfig
abstract val walletCardState: WalletCardState
abstract val warnings: ImmutableList<WalletNotification>
abstract val bottomSheetConfig: TangemBottomSheetConfig?
sealed class MultiCurrency : WalletState() {
abstract val tokensListState: WalletTokensListState
abstract val manageTokensButtonConfig: ManageTokensButtonConfig?
data class Content(
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val walletCardState: WalletCardState,
override val warnings: ImmutableList<WalletNotification>,
override val bottomSheetConfig: TangemBottomSheetConfig?,
override val tokensListState: WalletTokensListState,
override val manageTokensButtonConfig: ManageTokensButtonConfig?,
) : MultiCurrency()
data class Locked(
override val walletCardState: WalletCardState,
val onUnlockNotificationClick: () -> Unit,
val isBottomSheetShow: Boolean = false,
val onBottomSheetDismiss: () -> Unit = {},
val onUnlockClick: () -> Unit,
val onScanClick: () -> Unit,
) : MultiCurrency() {
override val pullToRefreshConfig: WalletPullToRefreshConfig
get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {})
override val warnings: ImmutableList<WalletNotification> = persistentListOf(
WalletNotification.UnlockWallets(onUnlockNotificationClick),
)
override val bottomSheetConfig = TangemBottomSheetConfig(
isShow = isBottomSheetShow,
onDismissRequest = onBottomSheetDismiss,
content = WalletBottomSheetConfig.UnlockWallets(
onUnlockClick = onUnlockClick,
onScanClick = onScanClick,
),
)
override val tokensListState = WalletTokensListState.ContentState.Locked
override val manageTokensButtonConfig = null
}
}
sealed class SingleCurrency : WalletState() {
abstract val buttons: PersistentList<WalletManageButton>
abstract val marketPriceBlockState: MarketPriceBlockState?
abstract val txHistoryState: TxHistoryState
data class Content(
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val walletCardState: WalletCardState,
override val warnings: ImmutableList<WalletNotification>,
override val bottomSheetConfig: TangemBottomSheetConfig?,
override val buttons: PersistentList<WalletManageButton>,
override val marketPriceBlockState: MarketPriceBlockState,
override val txHistoryState: TxHistoryState,
) : SingleCurrency()
data class Locked(
override val walletCardState: WalletCardState,
override val buttons: PersistentList<WalletManageButton>,
val onUnlockNotificationClick: () -> Unit,
val isBottomSheetShow: Boolean = false,
val onBottomSheetDismiss: () -> Unit = {},
val onUnlockClick: () -> Unit,
val onScanClick: () -> Unit,
val onExploreClick: () -> Unit,
) : SingleCurrency() {
override val pullToRefreshConfig: WalletPullToRefreshConfig
get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {})
override val warnings: ImmutableList<WalletNotification> = persistentListOf(
WalletNotification.UnlockWallets(onUnlockNotificationClick),
)
override val bottomSheetConfig = TangemBottomSheetConfig(
isShow = isBottomSheetShow,
onDismissRequest = onBottomSheetDismiss,
content = WalletBottomSheetConfig.UnlockWallets(
onUnlockClick = onUnlockClick,
onScanClick = onScanClick,
),
)
override val marketPriceBlockState: MarketPriceBlockState? = null
override val txHistoryState: TxHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
value = PagingData.from(
data = listOf(
TxHistoryState.TxHistoryItemState.Title(onExploreClick = onExploreClick),
TxHistoryState.TxHistoryItemState.Transaction(
state = TransactionState.Locked(txHash = "LOCKED_TX_HASH"),
),
),
),
),
)
}
}
}
internal sealed class WalletTokensListState {
object Empty : WalletTokensListState()
sealed class ContentState : WalletTokensListState() {
abstract val items: ImmutableList<TokensListItemState>
abstract val organizeTokensButtonConfig: OrganizeTokensButtonConfig?
object Loading : ContentState() {
override val items = persistentListOf<TokensListItemState>()
override val organizeTokensButtonConfig = null
}
data class Content(
override val items: ImmutableList<TokensListItemState>,
override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?,
) : ContentState()
object Locked : ContentState() {
override val items = persistentListOf(
TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)),
TokensListItemState.Token(state = TokenItemState.Locked(id = "Locked#1")),
)
override val organizeTokensButtonConfig = null
}
}
data class OrganizeTokensButtonConfig(val isEnabled: Boolean, val onClick: () -> Unit)
@Immutable
sealed class TokensListItemState {
abstract val id: Any
data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemState()
data class Token(val state: TokenItemState) : TokensListItemState() {
override val id: String = state.id
}
}
}
internal data class ManageTokensButtonConfig(val onClick: () -> Unit)

View file

@ -0,0 +1,54 @@
package com.tangem.feature.wallet.presentation.wallet.state2
import com.tangem.core.ui.event.consumedEvent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig
import com.tangem.feature.wallet.presentation.wallet.state2.transformers.WalletScreenStateTransformer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
import javax.inject.Singleton
/**
* Wallet state holder
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class WalletStateHolderV2 @Inject constructor() {
val uiState: StateFlow<WalletScreenState> get() = mutableUiState
val value: WalletScreenState get() = uiState.value
private val mutableUiState: MutableStateFlow<WalletScreenState> = MutableStateFlow(value = getInitialState())
fun update(function: (WalletScreenState) -> WalletScreenState) {
mutableUiState.update(function = function)
}
fun update(transformer: WalletScreenStateTransformer) {
mutableUiState.update(function = transformer::transform)
}
fun getSelectedWallet(): WalletState {
return with(value) { wallets[selectedWalletIndex] }
}
fun getSelectedWalletId(): UserWalletId {
return with(value) { wallets[selectedWalletIndex].walletCardState.id }
}
private fun getInitialState(): WalletScreenState {
return WalletScreenState(
onBackClick = {},
topBarConfig = WalletTopBarConfig(onDetailsClick = {}),
selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX,
wallets = persistentListOf(),
onWalletChange = {},
event = consumedEvent(),
isHidingMode = false,
)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.state2.transformers
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
internal interface WalletScreenStateTransformer {
fun transform(prevState: WalletScreenState): WalletScreenState
}

View file

@ -66,6 +66,7 @@ internal fun WalletEventEffect(
}
.addOnFailureListener(Timber::e)
}
is WalletEvent.DemonstrateWalletsScrollPreview -> Unit
}
},
)

View file

@ -0,0 +1,62 @@
package com.tangem.feature.wallet.presentation.wallet.ui
import android.widget.Toast
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
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.WalletAlertState
import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester
import com.tangem.feature.wallet.presentation.wallet.ui.utils.animateScrollByIndex
import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolling
@Suppress("LongParameterList")
@Composable
internal fun WalletEventEffectV2(
walletsListState: LazyListState,
snackbarHostState: SnackbarHostState,
event: StateEvent<WalletEvent>,
selectedWalletIndex: Int,
onAutoScrollSet: () -> Unit,
onAlertConfigSet: (WalletAlertState) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val resources = LocalContext.current.resources
val clipboardManager = LocalClipboardManager.current
EventEffect(
event = event,
onTrigger = { value ->
when (value) {
is WalletEvent.ChangeWallet -> {
onAutoScrollSet()
walletsListState.animateScrollByIndex(prevIndex = selectedWalletIndex, newIndex = value.index)
}
is WalletEvent.ShowError -> {
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources))
}
is WalletEvent.ShowToast -> {
Toast.makeText(context, value.text.resolveReference(resources), Toast.LENGTH_SHORT).show()
}
is WalletEvent.CopyAddress -> {
clipboardManager.setText(AnnotatedString(value.address))
Toast.makeText(context, value.toast.resolveReference(resources), Toast.LENGTH_SHORT).show()
}
is WalletEvent.ShowAlert -> onAlertConfigSet(value.state)
is WalletEvent.RateApp -> {
ReviewManagerRequester.request(context = context, onDismissClick = value.onDismissClick)
}
is WalletEvent.DemonstrateWalletsScrollPreview -> {
walletsListState.demonstrateScrolling(coroutineScope = coroutineScope, direction = value.direction)
}
}
},
)
}

View file

@ -0,0 +1,243 @@
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.LazyListScope
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.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state2.NOT_INITIALIZED_WALLET_INDEX
import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.*
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.controlButtons
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun WalletScreenV2(state: WalletScreenState) {
BackHandler(onBack = state.onBackClick)
// It means that screen is still initializing
if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return
val walletsListState = rememberLazyListState(initialFirstVisibleItemIndex = state.selectedWalletIndex)
val snackbarHostState = remember(::SnackbarHostState)
val isAutoScroll = remember { mutableStateOf(value = false) }
WalletContent(
state = state,
walletsListState = walletsListState,
snackbarHostState = snackbarHostState,
isAutoScroll = isAutoScroll,
onAutoScrollReset = { isAutoScroll.value = false },
)
var alertConfig by remember { mutableStateOf<WalletAlertState?>(value = null) }
alertConfig?.let {
WalletAlert(state = it, onDismiss = { alertConfig = null })
}
WalletEventEffectV2(
event = state.event,
selectedWalletIndex = state.selectedWalletIndex,
walletsListState = walletsListState,
snackbarHostState = snackbarHostState,
onAlertConfigSet = { alertConfig = it },
onAutoScrollSet = { isAutoScroll.value = true },
)
}
@Suppress("LongMethod")
@Composable
private fun WalletContent(
state: WalletScreenState,
walletsListState: LazyListState,
snackbarHostState: SnackbarHostState,
isAutoScroll: State<Boolean>,
onAutoScrollReset: () -> Unit,
) {
var selectedWalletIndex by remember { mutableIntStateOf(state.selectedWalletIndex) }
val selectedWallet = state.wallets[selectedWalletIndex]
BaseScaffold(state = state, selectedWallet = selectedWallet, snackbarHostState = snackbarHostState) {
val movableItemModifier = Modifier.changeWalletAnimator(walletsListState)
val lazyTxHistoryItems = (selectedWallet as? WalletState.SingleCurrency)?.let { walletState ->
(walletState.txHistoryState as? TxHistoryState.Content)?.contentItems?.collectAsLazyPagingItems()
}
val txHistoryItems by remember(selectedWallet.walletCardState.id, lazyTxHistoryItems?.itemCount) {
mutableStateOf(value = lazyTxHistoryItems)
}
val betweenItemsPadding = TangemTheme.dimens.spacing14
val horizontalPadding = TangemTheme.dimens.spacing16
val itemModifier = movableItemModifier
.padding(top = betweenItemsPadding)
.padding(horizontal = horizontalPadding)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing92,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item(
key = state.wallets.map { it.walletCardState.id },
contentType = state.wallets.map { it.walletCardState.id },
) {
WalletsList(
lazyListState = walletsListState,
wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(),
isBalanceHidden = state.isHidingMode,
)
}
(selectedWallet as? WalletState.SingleCurrency)?.let {
controlButtons(
configs = it.buttons,
selectedWalletIndex = selectedWalletIndex,
modifier = movableItemModifier.padding(top = betweenItemsPadding),
)
}
notifications(configs = selectedWallet.warnings, modifier = itemModifier)
(selectedWallet as? WalletState.SingleCurrency)?.let { walletState ->
walletState.marketPriceBlockState?.let { marketPriceBlockState ->
marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier)
}
}
contentItemsV2(
state = selectedWallet,
txHistoryItems = txHistoryItems,
isBalanceHidden = state.isHidingMode,
modifier = movableItemModifier,
)
organizeTokens(state = selectedWallet, itemModifier = itemModifier)
}
val bottomSheetConfig = selectedWallet.bottomSheetConfig
if (bottomSheetConfig != null) {
when (bottomSheetConfig.content) {
is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig)
is TokenReceiveBottomSheetConfig -> TokenReceiveBottomSheet(config = bottomSheetConfig)
is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig)
is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig)
}
}
WalletsListEffectsV2(
lazyListState = walletsListState,
selectedWalletIndex = selectedWalletIndex,
onWalletChange = state.onWalletChange,
onSelectedWalletIndexSet = { selectedWalletIndex = it },
isAutoScroll = isAutoScroll,
onAutoScrollReset = onAutoScrollReset,
)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun BaseScaffold(
state: WalletScreenState,
selectedWallet: WalletState,
snackbarHostState: SnackbarHostState,
content: @Composable () -> Unit,
) {
Scaffold(
topBar = { WalletTopBar(config = state.topBarConfig) },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
val manageTokensButtonConfig by remember(state.selectedWalletIndex) {
mutableStateOf(
(state.wallets[state.selectedWalletIndex] as? WalletState.MultiCurrency)?.manageTokensButtonConfig,
)
}
manageTokensButtonConfig?.let { ManageTokensButton(onClick = it.onClick) }
},
floatingActionButtonPosition = FabPosition.Center,
containerColor = TangemTheme.colors.background.secondary,
content = {
val pullRefreshState = rememberPullRefreshState(
refreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
onRefresh = selectedWallet.pullToRefreshConfig.onRefresh,
)
Box(
modifier = Modifier
.pullRefresh(pullRefreshState)
.padding(it),
) {
content()
WalletPullToRefreshIndicator(
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
},
)
}
@Composable
private fun ManageTokensButton(onClick: () -> Unit) {
PrimaryButton(
text = stringResource(id = R.string.main_manage_tokens),
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modifier) {
(state as? WalletState.MultiCurrency)?.let {
(state.tokensListState as? WalletTokensListState.ContentState)?.let {
it.organizeTokensButtonConfig?.let { config ->
organizeTokensButton(
modifier = itemModifier,
isEnabled = config.isEnabled,
onClick = config.onClick,
)
}
}
}
}

View file

@ -0,0 +1,45 @@
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.ui.utils.ScrollOffsetCollectorV2
import com.tangem.feature.wallet.presentation.wallet.ui.utils.WalletsListInteractionsCollector
@Suppress("LongParameterList")
@Composable
internal fun WalletsListEffectsV2(
lazyListState: LazyListState,
selectedWalletIndex: Int,
onWalletChange: (Int) -> Unit,
onSelectedWalletIndexSet: (Int) -> Unit,
isAutoScroll: State<Boolean>,
onAutoScrollReset: () -> Unit,
) {
LaunchedEffect(key1 = lazyListState, key2 = onWalletChange) {
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
.collect(
collector = ScrollOffsetCollectorV2(
selectedWalletIndex = selectedWalletIndex,
lazyListState = lazyListState,
onWalletChange = { newIndex ->
// Auto scroll must not change wallet
if (isAutoScroll.value) {
onSelectedWalletIndexSet(newIndex)
} else {
onSelectedWalletIndexSet(newIndex)
onWalletChange(newIndex)
}
},
),
)
}
LaunchedEffect(Unit) {
lazyListState.interactionSource.interactions.collect(
collector = WalletsListInteractionsCollector(onDragStart = onAutoScrollReset),
)
}
}

View file

@ -23,8 +23,10 @@ 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.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard
import kotlinx.collections.immutable.ImmutableList
private const val SHORT_SNAP_ELEMENT_COUNT = 50
@ -66,6 +68,40 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletsList(
lazyListState: LazyListState,
wallets: ImmutableList<WalletCardState>,
isBalanceHidden: Boolean,
) {
val horizontalCardPadding = TangemTheme.dimens.spacing16
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val itemWidth by remember(screenWidth) { derivedStateOf { screenWidth - horizontalCardPadding * 2 } }
LazyRow(
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
state = lazyListState,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState, itemWidth = itemWidth),
) {
items(
items = wallets,
key = { it.id.stringValue },
contentType = { it.id.stringValue },
) { state ->
WalletCard(
state = state,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.animateItemPlacement()
.width(itemWidth),
)
}
}
}
/**
* Custom implementation of fling behaviour that overrides 'shortSnapVelocityThreshold'.
* Every user's drag action will similar to a short snap

View file

@ -9,6 +9,8 @@ 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.ui.components.multicurrency.tokensListItems
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItemsV2
import com.tangem.feature.wallet.presentation.wallet.state2.WalletState as WalletStateV2
/**
* Wallet content
@ -29,4 +31,20 @@ internal fun LazyListScope.contentItems(
is WalletMultiCurrencyState -> tokensListItems(state.tokensListState, modifier, isBalanceHidden)
is WalletSingleCurrencyState -> txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier)
}
}
internal fun LazyListScope.contentItemsV2(
state: WalletStateV2,
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>?,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is WalletStateV2.MultiCurrency -> {
tokensListItemsV2(state.tokensListState, modifier, isBalanceHidden)
}
is WalletStateV2.SingleCurrency -> {
txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier)
}
}
}

View file

@ -22,7 +22,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
items(
items = configs,
key = { it::class.java },
contentType = { it.config::class.java },
contentType = { it::class.java },
itemContent = {
Notification(
config = it.config,

View file

@ -19,6 +19,8 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
import kotlinx.collections.immutable.ImmutableList
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState as WalletTokensListStateV2
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState as TokensListItemStateV2
private const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST"
@ -45,6 +47,23 @@ internal fun LazyListScope.tokensListItems(
}
}
internal fun LazyListScope.tokensListItemsV2(
state: WalletTokensListStateV2,
modifier: Modifier = Modifier,
isBalanceHidden: Boolean,
) {
when (state) {
is WalletTokensListStateV2.ContentState -> {
contentItemsV2(
items = state.items,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
)
}
WalletTokensListStateV2.Empty -> nonContentItem(modifier = modifier)
}
}
private fun LazyListScope.contentItems(
items: ImmutableList<WalletTokensListState.TokensListItemState>,
modifier: Modifier = Modifier,
@ -67,6 +86,28 @@ private fun LazyListScope.contentItems(
)
}
private fun LazyListScope.contentItemsV2(
items: ImmutableList<TokensListItemStateV2>,
modifier: Modifier = Modifier,
isBalanceHidden: Boolean,
) {
itemsIndexed(
items = items,
key = { _, item -> item.id },
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
MultiCurrencyContentItem(
state = item,
isBalanceHidden = isBalanceHidden,
modifier = modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = items.lastIndex,
),
)
},
)
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) {
item(

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem
import com.tangem.feature.wallet.presentation.common.component.TokenItem
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState
/**
* Multi-currency content item
@ -29,4 +30,20 @@ internal fun MultiCurrencyContentItem(
TokenItem(state = state.state, isBalanceHidden = isBalanceHidden, modifier = modifier)
}
}
}
@Composable
internal fun MultiCurrencyContentItem(
state: TokensListItemState,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is TokensListItemState.NetworkGroupTitle -> {
NetworkGroupItem(networkName = state.name.resolveReference(), modifier = modifier)
}
is TokensListItemState.Token -> {
TokenItem(state = state.state, isBalanceHidden = isBalanceHidden, modifier = modifier)
}
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.feature.wallet.presentation.wallet.ui.utils
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyListState
/**
* Animate scroll [LazyListState].
*
* [LazyListState] method for scroll with animation by index isn't supported custom animation.
* This extension method calculate offset between [prevIndex] and [newIndex],
* and scroll by it with default animation.
*/
internal suspend fun LazyListState.animateScrollByIndex(prevIndex: Int, newIndex: Int) {
animateScrollBy(
value = calculateOffset(layoutInfo, prevIndex, newIndex),
animationSpec = tween(durationMillis = 1000),
)
}
private fun calculateOffset(layoutInfo: LazyListLayoutInfo, prevIndex: Int, newIndex: Int): Float {
return layoutInfo.viewportSize.width.times(other = newIndex - prevIndex).toFloat()
}

View file

@ -0,0 +1,56 @@
package com.tangem.feature.wallet.presentation.wallet.ui.utils
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import com.google.android.play.core.review.ReviewInfo
import com.google.android.play.core.review.ReviewManager
import com.google.android.play.core.review.ReviewManagerFactory
import com.google.android.play.core.tasks.Task
import timber.log.Timber
internal object ReviewManagerRequester {
fun request(context: Context, onDismissClick: () -> Unit) {
val reviewManager = ReviewManagerFactory.create(context)
val requestTask = reviewManager.requestReviewFlow()
requestTask
.addOnCompleteListener {
handleOnCompleteRequestTask(
reviewManager = reviewManager,
activity = context.findActivity(),
task = it,
onDismissClick = onDismissClick,
)
}
.addOnFailureListener(Timber::e)
}
private fun handleOnCompleteRequestTask(
reviewManager: ReviewManager,
activity: Activity,
task: Task<ReviewInfo>,
onDismissClick: () -> Unit,
) {
if (task.isSuccessful) {
val reviewFlow = reviewManager.launchReviewFlow(activity, task.result)
reviewFlow
.addOnCompleteListener { resultReviewTask ->
if (!resultReviewTask.isSuccessful) onDismissClick()
}
.addOnFailureListener(Timber::e)
} else {
Timber.e(task.exception)
}
}
private fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
error("Permissions should be called in the context of an Activity")
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.feature.wallet.presentation.wallet.ui.utils
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
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 change selected wallet index.
* If last visible item offset is greater than half item size, then change selected wallet index.
*
* @param selectedWalletIndex selected wallet index
* @property lazyListState lazy list state
* @property onWalletChange callback that will be invoked on wallet change
*
[REDACTED_AUTHOR]
*/
internal class ScrollOffsetCollectorV2(
selectedWalletIndex: Int,
private val lazyListState: LazyListState,
private val onWalletChange: (Int) -> Unit,
) : FlowCollector<List<LazyListItemInfo>> {
private val LazyListItemInfo.halfItemSize
get() = size.div(other = 2)
private var currentIndex = selectedWalletIndex
override suspend fun emit(value: List<LazyListItemInfo>) {
if (!lazyListState.isScrollInProgress || value.size <= 1) return
val firstItem = value.firstOrNull() ?: return
val lastItem = value.lastOrNull() ?: return
if (abs(firstItem.offset) > firstItem.halfItemSize) {
selectIndex(newIndex = firstItem.index + 1)
} else if (abs(lastItem.offset) > lastItem.halfItemSize) {
selectIndex(newIndex = lastItem.index - 1)
}
}
private fun selectIndex(newIndex: Int) {
if (currentIndex != newIndex) {
currentIndex = newIndex
onWalletChange(newIndex)
}
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.feature.wallet.presentation.wallet.ui.utils
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.lazy.LazyListState
import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent.DemonstrateWalletsScrollPreview
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
private const val VISIBLE_PART_OF_WALLET_CARD = 0.2f
internal fun LazyListState.demonstrateScrolling(
coroutineScope: CoroutineScope,
direction: DemonstrateWalletsScrollPreview.Direction,
) {
coroutineScope.launch {
animateScrollBy(
value = calculateOffset(direction = direction, isReverse = false),
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
)
}
.invokeOnCompletion {
coroutineScope.launch {
animateScrollBy(
value = calculateOffset(direction = direction, isReverse = true),
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow,
),
)
}
}
}
private fun LazyListState.calculateOffset(
direction: DemonstrateWalletsScrollPreview.Direction,
isReverse: Boolean,
): Float {
val sign = when (direction) {
DemonstrateWalletsScrollPreview.Direction.LEFT -> 1
DemonstrateWalletsScrollPreview.Direction.RIGHT -> -1
}.times(other = if (isReverse) -1 else 1)
return layoutInfo.viewportSize.width.toFloat() * VISIBLE_PART_OF_WALLET_CARD * sign
}