Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-26 15:59:36 +08:00
parent 4ee802212b
commit d2e036e574
12 changed files with 133 additions and 58 deletions

View file

@ -27,6 +27,7 @@ dependencies {
implementation(deps.compose.material3)
implementation(deps.compose.paging)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.ui.utils)
/** Other libraries */
implementation(deps.compose.accompanist.systemUiController)

View file

@ -1,18 +1,27 @@
package com.tangem.core.ui.components.transactions
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.util.fastForEach
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.ImmutableList
/**
* LazyList extension for transactions history [TxHistoryState]
@ -45,6 +54,12 @@ fun LazyListScope.txHistoryItems(
)
}
is TxHistoryState.NotSupported -> {
if (state.pendingTransactions.isNotEmpty()) {
item(key = "PendingTxsBlock", contentType = "PendingTxsBlock") {
PendingTxsBlock(pendingTxs = state.pendingTransactions, isBalanceHidden = isBalanceHidden)
}
}
nonContentItem(
state = EmptyTransactionsBlockState.NotImplemented(onClick = state.onExploreClick),
modifier = modifier,
@ -86,6 +101,21 @@ private fun LazyListScope.contentItems(
)
}
@Composable
private fun PendingTxsBlock(pendingTxs: ImmutableList<TransactionState>, isBalanceHidden: Boolean) {
Column(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing16)
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.background(color = TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
horizontalAlignment = Alignment.Start,
) {
pendingTxs.fastForEach { Transaction(state = it, isBalanceHidden = isBalanceHidden) }
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.nonContentItem(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) {
item(key = state::class.java, contentType = state::class.java) {

View file

@ -1,6 +1,7 @@
package com.tangem.core.ui.components.transactions.state
import androidx.paging.PagingData
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
/** Wallet transaction history state */
@ -19,9 +20,13 @@ sealed interface TxHistoryState {
/**
* Not supported tx history state
*
* @property onExploreClick lambda be invoke when explore button was clicked
* @property pendingTransactions pending transactions
* @property onExploreClick lambda be invoke when explore button was clicked
*/
data class NotSupported(val onExploreClick: () -> Unit) : TxHistoryState
data class NotSupported(
val pendingTransactions: ImmutableList<TransactionState>,
val onExploreClick: () -> Unit,
) : TxHistoryState
/**
* Error state

View file

@ -4,7 +4,6 @@ import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import arrow.core.right
import android.content.res.AssetManager
import com.squareup.moshi.Moshi
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.blockchains.solana.RentProvider
@ -39,7 +38,6 @@ import java.math.BigDecimal
@Suppress("LargeClass")
// FIXME: Move to its own module and make internal
@Deprecated("Inject the WalletManagerFacade interface using DI instead")
@Suppress("LargeClass")
class DefaultWalletManagersFacade(
private val walletManagersStore: WalletManagersStore,
private val userWalletsStore: UserWalletsStore,

View file

@ -5,6 +5,7 @@ import com.tangem.common.Provider
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyStatusError
@ -47,10 +48,16 @@ internal class TokenDetailsLoadedBalanceConverter(
private fun convert(status: CryptoCurrencyStatus): TokenDetailsState {
val state = currentStateProvider()
val currencyName = state.marketPriceBlockState.currencySymbol
val pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList()
return state.copy(
tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status),
marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName),
pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList(),
pendingTxs = pendingTxs,
txHistoryState = if (state.txHistoryState is TxHistoryState.NotSupported) {
state.txHistoryState.copy(pendingTransactions = pendingTxs)
} else {
state.txHistoryState
},
)
}

View file

@ -8,6 +8,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
@ -25,6 +26,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
@ -64,7 +66,10 @@ internal class TokenDetailsStateFactory(
}
private val loadingTransactionsStateConverter by lazy {
TokenDetailsLoadingTxHistoryConverter(currentStateProvider = currentStateProvider, clickIntents = clickIntents)
TokenDetailsLoadingTxHistoryConverter(
currentStateProvider = currentStateProvider,
clickIntents = clickIntents,
)
}
private val loadedTxHistoryConverter by lazy {
@ -106,8 +111,16 @@ internal class TokenDetailsStateFactory(
)
}
fun getLoadingTxHistoryState(itemsCountEither: Either<TxHistoryStateError, Int>): TokenDetailsState {
return loadingTransactionsStateConverter.convert(value = itemsCountEither)
fun getLoadingTxHistoryState(
itemsCountEither: Either<TxHistoryStateError, Int>,
pendingTransactions: List<TransactionState>,
): TokenDetailsState {
return loadingTransactionsStateConverter.convert(
value = TokenDetailsLoadingTxHistoryModel(
historyLoadingState = itemsCountEither,
pendingTransactions = pendingTransactions,
),
)
}
fun getLoadedTxHistoryState(

View file

@ -7,27 +7,38 @@ import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
internal class TokenDetailsLoadingTxHistoryConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
private val clickIntents: TokenDetailsClickIntents,
) : Converter<Either<TxHistoryStateError, Int>, TokenDetailsState> {
) : Converter<TokenDetailsLoadingTxHistoryModel, TokenDetailsState> {
override fun convert(value: Either<TxHistoryStateError, Int>): TokenDetailsState {
return value.fold(ifLeft = ::convertError, ifRight = ::convert)
override fun convert(value: TokenDetailsLoadingTxHistoryModel): TokenDetailsState {
return value.historyLoadingState.fold(
ifLeft = { convertError(error = it, pendingTransactions = value.pendingTransactions) },
ifRight = ::convert,
)
}
private fun convertError(error: TxHistoryStateError): TokenDetailsState {
private fun convertError(
error: TxHistoryStateError,
pendingTransactions: List<TransactionState>,
): TokenDetailsState {
return currentStateProvider().copy(
txHistoryState = when (error) {
is TxHistoryStateError.EmptyTxHistories -> TxHistoryState.Empty
is TxHistoryStateError.DataError -> TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick)
is TxHistoryStateError.TxHistoryNotImplemented -> {
TxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick)
TxHistoryState.NotSupported(
pendingTransactions = pendingTransactions.toImmutableList(),
onExploreClick = clickIntents::onExploreClick,
)
}
},
)
@ -59,4 +70,9 @@ internal class TokenDetailsLoadingTxHistoryConverter(
}
}
}
data class TokenDetailsLoadingTxHistoryModel(
val historyLoadingState: Either<TxHistoryStateError, Int>,
val pendingTransactions: List<TransactionState>,
)
}

View file

@ -2,7 +2,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@ -17,10 +16,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
@ -29,8 +26,6 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.transactions.Transaction
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.core.ui.event.EventEffect
@ -45,7 +40,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
import kotlinx.collections.immutable.PersistentList
// TODO: Split to blocks [REDACTED_JIRA]
@Suppress("LongMethod")
@ -122,15 +116,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) },
)
}
if (state.txHistoryState is TxHistoryState.NotSupported && state.pendingTxs.isNotEmpty()) {
item {
PendingTxsBlock(
pendingTxs = state.pendingTxs,
isBalanceHidden = state.isBalanceHidden,
modifier = itemModifier,
)
}
}
txHistoryItems(
state = state.txHistoryState,
isBalanceHidden = state.isBalanceHidden,
@ -162,23 +148,6 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
}
}
@Composable
private fun PendingTxsBlock(
pendingTxs: PersistentList<TransactionState>,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.background(color = TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
horizontalAlignment = Alignment.Start,
) {
pendingTxs.fastForEach { Transaction(state = it, isBalanceHidden = isBalanceHidden) }
}
}
@Composable
internal fun TokenDetailsEventEffect(snackbarHostState: SnackbarHostState, event: StateEvent<TextReference>) {
val resources = LocalContext.current.resources

View file

@ -199,7 +199,10 @@ internal class TokenDetailsViewModel @Inject constructor(
// if countEither is left, handling error state run inside getLoadingTxHistoryState
if (showItemsLoading || txHistoryItemsCountEither.isLeft()) {
uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither)
uiState = stateFactory.getLoadingTxHistoryState(
itemsCountEither = txHistoryItemsCountEither,
pendingTransactions = uiState.pendingTxs,
)
}
txHistoryItemsCountEither.onRight {

View file

@ -92,6 +92,7 @@ internal class WalletStateFactory(
private val loadingTransactionsStateConverter by lazy {
WalletLoadingTxHistoryConverter(
currentStateProvider = currentStateProvider,
currentCardTypeResolverProvider = currentCardTypeResolverProvider,
clickIntents = clickIntents,
)
}
@ -225,9 +226,15 @@ internal class WalletStateFactory(
)
}
fun getLoadingTxHistoryState(itemsCountEither: Either<TxHistoryStateError, Int>): WalletState {
fun getLoadingTxHistoryState(
itemsCountEither: Either<TxHistoryStateError, Int>,
pendingTransactions: Set<TxHistoryItem>,
): WalletState {
return loadingTransactionsStateConverter.convert(
WalletLoadingTxHistoryConverter.WalletLoadingTxHistoryModel(historyLoadingState = itemsCountEither),
WalletLoadingTxHistoryConverter.WalletLoadingTxHistoryModel(
historyLoadingState = itemsCountEither,
pendingTransactions = pendingTransactions,
),
)
}

View file

@ -6,10 +6,13 @@ import com.tangem.common.Converter
import com.tangem.common.Provider
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState.*
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryStateError
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.viewmodels.WalletClickIntents
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@ -23,17 +26,27 @@ import kotlinx.coroutines.flow.update
*/
internal class WalletLoadingTxHistoryConverter(
private val currentStateProvider: Provider<WalletState>,
private val currentCardTypeResolverProvider: Provider<CardTypesResolver>,
private val clickIntents: WalletClickIntents,
) : Converter<WalletLoadingTxHistoryConverter.WalletLoadingTxHistoryModel, WalletState> {
private val txHistoryItemConverter by lazy {
val blockchain = currentCardTypeResolverProvider().getBlockchain()
WalletTxHistoryTransactionStateConverter(
symbol = blockchain.currency,
decimals = blockchain.decimals(),
clickIntents = clickIntents,
)
}
override fun convert(value: WalletLoadingTxHistoryModel): WalletState {
return value.historyLoadingState.fold(
ifLeft = ::convertError,
ifLeft = { convertError(it, value.pendingTransactions) },
ifRight = ::convertRight,
)
}
private fun convertError(error: TxHistoryStateError): WalletState {
private fun convertError(error: TxHistoryStateError, pendingTransactions: Set<TxHistoryItem>): WalletState {
val state = currentStateProvider()
return if (state is WalletSingleCurrencyState.Content) {
@ -42,7 +55,11 @@ internal class WalletLoadingTxHistoryConverter(
is TxHistoryStateError.EmptyTxHistories -> Empty
is TxHistoryStateError.DataError -> Error(onReloadClick = clickIntents::onReloadClick)
is TxHistoryStateError.TxHistoryNotImplemented -> {
NotSupported(onExploreClick = clickIntents::onExploreClick)
NotSupported(
pendingTransactions = txHistoryItemConverter.convertList(pendingTransactions)
.toImmutableList(),
onExploreClick = clickIntents::onExploreClick,
)
}
},
)
@ -78,5 +95,8 @@ internal class WalletLoadingTxHistoryConverter(
}
}
data class WalletLoadingTxHistoryModel(val historyLoadingState: Either<TxHistoryStateError, Int>)
data class WalletLoadingTxHistoryModel(
val historyLoadingState: Either<TxHistoryStateError, Int>,
val pendingTransactions: Set<TxHistoryItem>,
)
}

View file

@ -768,6 +768,8 @@ internal class WalletViewModel @Inject constructor(
refreshSingleCurrencyContent(selectedWalletIndex)
}
// FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary
// currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged()
private fun refreshSingleCurrencyContent(walletIndex: Int) {
uiState = stateFactory.getRefreshingState()
val wallet = getWallet(walletIndex)
@ -785,9 +787,12 @@ internal class WalletViewModel @Inject constructor(
val singleCurrencyState = uiState as WalletSingleCurrencyState
if (singleCurrencyState.txHistoryState !is TxHistoryState.Content) {
// show loading indicator while refreshing in non content state
uiState = stateFactory.getLoadingTxHistoryState(1.right())
uiState = stateFactory.getLoadingTxHistoryState(
itemsCountEither = 1.right(),
pendingTransactions = it.value.pendingTransactions,
)
}
updateTxHistory(wallet.walletId, it.currency, refresh = true)
updateTxHistory(userWalletId = wallet.walletId, currencyStatus = it, refresh = true)
}
}.saveIn(refreshContentJobHolder)
}
@ -1218,7 +1223,7 @@ internal class WalletViewModel @Inject constructor(
updateNotifications(index)
updateButtons(userWallet = wallet, currencyStatus = status)
updateTxHistory(wallet.walletId, status.currency, refresh = false)
updateTxHistory(userWalletId = wallet.walletId, currencyStatus = status, refresh = false)
}
}
.flowOn(dispatchers.io)
@ -1274,22 +1279,23 @@ internal class WalletViewModel @Inject constructor(
}
}
private fun updateTxHistory(userWalletId: UserWalletId, currency: CryptoCurrency, refresh: Boolean) {
private fun updateTxHistory(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus, refresh: Boolean) {
viewModelScope.launch(dispatchers.io) {
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
userWalletId = userWalletId,
network = currency.network,
network = currencyStatus.currency.network,
)
uiState = stateFactory.getLoadingTxHistoryState(
itemsCountEither = txHistoryItemsCountEither,
pendingTransactions = currencyStatus.value.pendingTransactions,
)
txHistoryItemsCountEither.onRight {
uiState = stateFactory.getLoadedTxHistoryState(
txHistoryEither = txHistoryItemsUseCase(
userWalletId = userWalletId,
currency = currency,
currency = currencyStatus.currency,
refresh = refresh,
).map {
it.cachedIn(viewModelScope)