Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-16 22:46:06 +04:00
parent 74e4b644f7
commit 0680fb9e7e
9 changed files with 64 additions and 569 deletions

View file

@ -1,220 +0,0 @@
package com.tangem.features.txhistory.converter
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.network.TxInfo.TransactionType
import com.tangem.features.txhistory.impl.R
import com.tangem.features.txhistory.utils.TxHistoryUiActions
import com.tangem.utils.StringsSigns
import com.tangem.utils.annotations.RemoveWithToggle
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import com.tangem.utils.toBriefAddressFormat
@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]. Produces pre-redesign TransactionState.")
@RemoveWithToggle("APP_REDESIGN_ENABLED")
internal class TxHistoryItemToTransactionStateConverter(
private val currency: CryptoCurrency,
private val txHistoryUiActions: TxHistoryUiActions,
) : Converter<TxInfo, TransactionState> {
override fun convert(value: TxInfo): TransactionState {
return TransactionState.Content(
txHash = value.txHash,
amount = value.getAmount(),
time = value.timestampInMillis.toTimeFormat(),
status = value.status.toUiStatus(),
direction = value.extractDirection(),
iconRes = value.extractIcon(),
title = value.extractTitle(),
subtitle = value.extractSubtitle(),
timestamp = value.timestampInMillis,
onClick = { txHistoryUiActions.openTxInExplorer(value.txHash) },
)
}
private fun TxInfo.extractIcon(): Int = if (status == TxInfo.TransactionStatus.Failed) {
R.drawable.ic_close_24
} else {
when (type) {
is TransactionType.YieldSupply.DeployContract,
is TransactionType.Approve,
-> R.drawable.ic_doc_24
is TransactionType.Staking.Stake,
is TransactionType.Staking.Vote,
is TransactionType.Staking.Restake,
-> R.drawable.ic_transaction_history_staking_24
is TransactionType.Staking.ClaimRewards,
-> R.drawable.ic_transaction_history_claim_rewards_24
is TransactionType.Staking.Unstake,
is TransactionType.Staking.Withdraw,
-> R.drawable.ic_transaction_history_unstaking_24
is TransactionType.YieldSupply.Enter -> R.drawable.ic_connect_24
is TransactionType.YieldSupply.InitializeToken -> R.drawable.ic_gear_24
is TransactionType.YieldSupply.ReactivateToken -> R.drawable.ic_refresh_24
is TransactionType.YieldSupply.Exit -> R.drawable.ic_disconnect_24
is TransactionType.Operation,
is TransactionType.Swap,
is TransactionType.Transfer,
is TransactionType.UnknownOperation,
is TransactionType.YieldSupply.Send,
TransactionType.YieldSupply.Topup,
TransactionType.GaslessFee,
-> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
}
}
@Suppress("CyclomaticComplexMethod")
private fun TxInfo.extractTitle(): TextReference = when (val type = type) {
is TransactionType.Approve -> resourceReference(R.string.common_approval)
is TransactionType.Operation -> stringReference(type.name)
is TransactionType.Swap -> resourceReference(R.string.common_swap)
is TransactionType.Transfer -> resourceReference(R.string.common_transfer)
is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake)
is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake)
is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote)
is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards)
is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw)
is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake)
is TransactionType.YieldSupply -> when (type) {
is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter)
is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit)
TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup)
is TransactionType.YieldSupply.Send -> {
if (type.isYieldSupplyWithdraw || isOutgoing) {
resourceReference(R.string.yield_module_transaction_withdraw)
} else {
resourceReference(R.string.common_transfer)
}
}
is TransactionType.YieldSupply.DeployContract -> resourceReference(
R.string
.yield_module_transaction_deploy_contract,
)
is TransactionType.YieldSupply.InitializeToken -> resourceReference(
R.string
.yield_module_transaction_initialize,
)
is TransactionType.YieldSupply.ReactivateToken -> resourceReference(
R.string
.yield_module_transaction_reactivate,
)
}
is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation)
TransactionType.GaslessFee -> resourceReference(R.string.gasless_transaction_fee)
}
private fun TxInfo.extractSubtitle(): TextReference {
return when (val type = this.type) {
is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Coin) {
if (type is TransactionType.YieldSupply.Send) {
extractSubtitleByAddressType()
} else {
resourceReference(
R.string.transaction_history_transaction_for_address,
wrappedList(type.address?.toBriefAddressFormat().orEmpty()),
)
}
} else {
when (type) {
is TransactionType.YieldSupply.Enter -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(R.string.yield_module_transaction_enter_subtitle, wrappedList(amount))
}
TransactionType.YieldSupply.Topup -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(R.string.yield_module_transaction_topup_subtitle, wrappedList(amount))
}
is TransactionType.YieldSupply.Exit -> {
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount))
}
is TransactionType.YieldSupply.Send -> {
if (isOutgoing || !type.isYieldSupplyWithdraw) {
extractSubtitleByAddressType()
} else {
val amount =
amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(
R.string.yield_module_transaction_exit_subtitle,
wrappedList(amount),
)
}
}
else -> extractSubtitleByAddressType()
}
}
else -> extractSubtitleByAddressType()
}
}
private fun TxInfo.extractSubtitleByAddressType(): TextReference =
when (val interactionAddress = interactionAddressType) {
is TxInfo.InteractionAddressType.Contract -> resourceReference(
id = R.string.transaction_history_contract_address,
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
)
is TxInfo.InteractionAddressType.Multiple -> resourceReference(
id = if (isOutgoing) {
R.string.transaction_history_transaction_to_address
} else {
R.string.transaction_history_transaction_from_address
},
formatArgs = wrappedList(resourceReference(R.string.transaction_history_multiple_addresses)),
)
is TxInfo.InteractionAddressType.User -> resourceReference(
id = if (isOutgoing) {
R.string.transaction_history_transaction_to_address
} else {
R.string.transaction_history_transaction_from_address
},
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
)
is TxInfo.InteractionAddressType.Validator -> resourceReference(
id = R.string.transaction_history_transaction_validator,
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
)
null -> {
TextReference.EMPTY
}
}
private fun TxInfo.extractDirection() =
if (isOutgoing) TransactionState.Content.Direction.OUTGOING else TransactionState.Content.Direction.INCOMING
@Suppress("ComplexCondition")
private fun TxInfo.getAmount(): String {
when (type) {
is TransactionType.Staking.Vote,
TransactionType.Staking.ClaimRewards,
TransactionType.Staking.Withdraw,
-> return ""
is TransactionType.YieldSupply -> {
if (currency is CryptoCurrency.Token && type == TransactionType.YieldSupply.Send && !isOutgoing) {
return ""
}
}
else -> Unit
}
val prefix = when {
status == TxInfo.TransactionStatus.Failed -> ""
this.amount.isZero() -> ""
else -> if (isOutgoing) StringsSigns.MINUS else StringsSigns.PLUS
}
return prefix + amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
}
private fun TxInfo.TransactionStatus.toUiStatus() = when (this) {
TxInfo.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed
TxInfo.TransactionStatus.Failed -> TransactionState.Content.Status.Failed
TxInfo.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus
@ -27,7 +26,6 @@ import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.converter.ExpressTxToTransactionItemUMConverter
import com.tangem.features.txhistory.converter.TxHistoryInfoToTransactionItemUMConverter
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionItemUMConverter
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUpdateListener
import com.tangem.features.txhistory.state.TxHistoryItemsSnapshot
@ -56,7 +54,6 @@ internal class TxHistoryModel @Inject constructor(
private val urlOpener: UrlOpener,
private val txHistoryUpdateListener: TxHistoryUpdateListener,
private val stateController: TxHistoryStateController,
private val designFeatureToggles: DesignFeatureToggles,
private val txHistoryFeatureToggle: TxHistoryFeatureToggles,
private val historyTxListManagerFactory: HistoryTxListManager.Factory,
private val appTxHistoryFetcher: AppTxHistoryFetcher,
@ -67,17 +64,9 @@ internal class TxHistoryModel @Inject constructor(
private val params: TxHistoryComponent.Params = paramsContainer.require()
private val lookupDataFlow: Flow<TxHistoryLookupContext> = if (designFeatureToggles.isRedesignEnabled) {
ownerLookupProducer()
.flowOn(dispatchers.default)
.shareIn(modelScope, SharingStarted.WhileSubscribed(), replay = 1)
} else {
emptyFlow()
}
@RemoveWithToggle("APP_REDESIGN_ENABLED")
private val legacyTxHistoryItemConverter =
TxHistoryItemToTransactionStateConverter(currency = params.currency, txHistoryUiActions = this)
private val lookupDataFlow: Flow<TxHistoryLookupContext> = ownerLookupProducer()
.flowOn(dispatchers.default)
.shareIn(modelScope, SharingStarted.WhileSubscribed(), replay = 1)
@RemoveWithToggle("AND_15767_NEW_TX_HISTORY_ENABLED")
private val txHistoryListManager: TxHistoryListManager? = if (!txHistoryFeatureToggle.isNewTxHistoryEnabled) {
@ -86,10 +75,8 @@ internal class TxHistoryModel @Inject constructor(
dispatchers = dispatchers,
userWalletId = params.userWalletId,
currency = params.currency,
designFeatureToggles = designFeatureToggles,
txHistoryUiActions = this,
lookupDataFlow = lookupDataFlow,
legacyTxHistoryItemConverter = legacyTxHistoryItemConverter,
)
} else {
null
@ -297,11 +284,7 @@ internal class TxHistoryModel @Inject constructor(
.distinctUntilChanged()
val combined: Flow<Pair<Option<CryptoCurrencyStatus>, TxHistoryLookupContext?>> =
if (designFeatureToggles.isRedesignEnabled) {
combine(statusFlow, lookupDataFlow) { status, lookup -> status to lookup }
} else {
statusFlow.map { it to null }
}
combine(statusFlow, lookupDataFlow) { status, lookup -> status to lookup }
combined
.onEach { (status, lookup) -> handlePendingTxsChanges(status, lookup) }
@ -324,7 +307,6 @@ internal class TxHistoryModel @Inject constructor(
)
pending.map(converter::convert).toPersistentList()
},
legacyPendingTxs = { pending.map(legacyTxHistoryItemConverter::convert).toPersistentList() },
)
}
}

View file

@ -1,17 +1,12 @@
package com.tangem.features.txhistory.state
import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM
import kotlinx.collections.immutable.ImmutableList
/**
* Snapshot of transaction history items emitted by [TxHistoryListManager]. Wraps either the
* primary or legacy item list so that one [Flow] can carry both pipelines, with the active
* variant chosen via the design feature toggle.
* Snapshot of transaction history items emitted by [TxHistoryListManager].
*/
internal sealed interface TxHistoryItemsSnapshot {
data class Items(val items: ImmutableList<TxHistoryItemsUM.TxHistoryItemUM>) : TxHistoryItemsSnapshot
data class LegacyItems(val items: ImmutableList<TxHistoryUM.TxHistoryItemUM>) : TxHistoryItemsSnapshot
}

View file

@ -1,113 +1,71 @@
package com.tangem.features.txhistory.state
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
/**
* Owns the transaction history UI state and routes updates to either [legacyUiState] or
* [uiState] based on [DesignFeatureToggles.isRedesignEnabled]. Only the active pipeline gets
* emitted to; the inactive flow stays at its initial Loading value.
* Owns the transaction history UI state and routes updates to [uiState].
*/
@ModelScoped
internal class TxHistoryStateController @Inject constructor(
private val designFeatureToggles: DesignFeatureToggles,
) {
internal class TxHistoryStateController @Inject constructor() {
private val _legacyUiState: MutableStateFlow<TxHistoryUM> =
MutableStateFlow(TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = {}))
val legacyUiState: StateFlow<TxHistoryUM> = _legacyUiState
/**
* Pre-redesign state. Kept only to satisfy consumers that still read the legacy UI (they are
* dead at runtime while the redesign is enabled); no longer populated.
*/
val legacyUiState: StateFlow<TxHistoryUM> =
MutableStateFlow(TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = {})).asStateFlow()
private val _uiState: MutableStateFlow<TxHistoryItemsUM> =
MutableStateFlow(TxHistoryItemsUM.Loading(isBalanceHidden = true, onExploreClick = {}))
val uiState: StateFlow<TxHistoryItemsUM> = _uiState
val isNotSupported: Boolean
get() = if (designFeatureToggles.isRedesignEnabled) {
_uiState.value is TxHistoryItemsUM.NotSupported
} else {
_legacyUiState.value is TxHistoryUM.NotSupported
}
get() = _uiState.value is TxHistoryItemsUM.NotSupported
fun setLoading(isBalanceHidden: Boolean, onExploreClick: () -> Unit) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.value = TxHistoryItemsUM.Loading(
isBalanceHidden = isBalanceHidden,
onExploreClick = onExploreClick,
)
} else {
_legacyUiState.value = TxHistoryUM.Loading(
isBalanceHidden = isBalanceHidden,
onExploreClick = onExploreClick,
)
}
_uiState.value = TxHistoryItemsUM.Loading(
isBalanceHidden = isBalanceHidden,
onExploreClick = onExploreClick,
)
}
fun setLoadingIfNotContent(onExploreClick: () -> Unit) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.update { state ->
state as? TxHistoryItemsUM.Content ?: TxHistoryItemsUM.Loading(state.isBalanceHidden, onExploreClick)
}
} else {
_legacyUiState.update { state ->
state as? TxHistoryUM.Content ?: TxHistoryUM.Loading(state.isBalanceHidden, onExploreClick)
}
_uiState.update { state ->
state as? TxHistoryItemsUM.Content ?: TxHistoryItemsUM.Loading(state.isBalanceHidden, onExploreClick)
}
}
fun setError(onReloadClick: () -> Unit, onExploreClick: () -> Unit) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.value = TxHistoryItemsUM.Error(
isBalanceHidden = _uiState.value.isBalanceHidden,
onReloadClick = onReloadClick,
onExploreClick = onExploreClick,
)
} else {
_legacyUiState.value = TxHistoryUM.Error(
isBalanceHidden = _legacyUiState.value.isBalanceHidden,
onReloadClick = onReloadClick,
onExploreClick = onExploreClick,
)
}
_uiState.value = TxHistoryItemsUM.Error(
isBalanceHidden = _uiState.value.isBalanceHidden,
onReloadClick = onReloadClick,
onExploreClick = onExploreClick,
)
}
fun setEmpty(onExploreClick: () -> Unit) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.value = TxHistoryItemsUM.Empty(
isBalanceHidden = _uiState.value.isBalanceHidden,
onExploreClick = onExploreClick,
)
} else {
_legacyUiState.value = TxHistoryUM.Empty(
isBalanceHidden = _legacyUiState.value.isBalanceHidden,
onExploreClick = onExploreClick,
)
}
_uiState.value = TxHistoryItemsUM.Empty(
isBalanceHidden = _uiState.value.isBalanceHidden,
onExploreClick = onExploreClick,
)
}
fun setNotSupported(onExploreClick: () -> Unit) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.value = TxHistoryItemsUM.NotSupported(
isBalanceHidden = _uiState.value.isBalanceHidden,
pendingTransactions = persistentListOf(),
onExploreClick = onExploreClick,
)
} else {
_legacyUiState.value = TxHistoryUM.NotSupported(
isBalanceHidden = _legacyUiState.value.isBalanceHidden,
pendingTransactions = persistentListOf(),
onExploreClick = onExploreClick,
)
}
_uiState.value = TxHistoryItemsUM.NotSupported(
isBalanceHidden = _uiState.value.isBalanceHidden,
pendingTransactions = persistentListOf(),
onExploreClick = onExploreClick,
)
}
fun setContent(snapshot: TxHistoryItemsSnapshot, loadMore: () -> Boolean, onExploreClick: () -> Unit) {
@ -129,27 +87,10 @@ internal class TxHistoryStateController @Inject constructor(
)
}
}
is TxHistoryItemsSnapshot.LegacyItems -> _legacyUiState.update { state ->
if (snapshot.items.none { it is TxHistoryUM.TxHistoryItemUM.Transaction }) {
TxHistoryUM.Empty(
isBalanceHidden = state.isBalanceHidden,
onExploreClick = onExploreClick,
)
} else if (state is TxHistoryUM.Content) {
state.copy(items = snapshot.items)
} else {
TxHistoryUM.Content(
items = snapshot.items,
isBalanceHidden = state.isBalanceHidden,
loadMore = loadMore,
)
}
}
}
}
fun updateLoadingMore(isLoadingMore: Boolean) {
if (!designFeatureToggles.isRedesignEnabled) return
_uiState.update { state ->
if (state is TxHistoryItemsUM.Content && state.isLoadingMore != isLoadingMore) {
state.copy(isLoadingMore = isLoadingMore)
@ -160,32 +101,15 @@ internal class TxHistoryStateController @Inject constructor(
}
fun updateBalanceHidden(isBalanceHidden: Boolean) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.update { state -> state.copySealed(isBalanceHidden = isBalanceHidden) }
} else {
_legacyUiState.update { state -> state.copySealed(isBalanceHidden = isBalanceHidden) }
}
_uiState.update { state -> state.copySealed(isBalanceHidden = isBalanceHidden) }
}
fun updatePendingTransactions(
pendingTxs: () -> ImmutableList<TransactionItemUM>,
legacyPendingTxs: () -> ImmutableList<TransactionState>,
) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.update { state ->
if (state is TxHistoryItemsUM.NotSupported) {
state.copy(pendingTransactions = pendingTxs())
} else {
state
}
}
} else {
_legacyUiState.update { state ->
if (state is TxHistoryUM.NotSupported) {
state.copy(pendingTransactions = legacyPendingTxs())
} else {
state
}
fun updatePendingTransactions(pendingTxs: () -> ImmutableList<TransactionItemUM>) {
_uiState.update { state ->
if (state is TxHistoryItemsUM.NotSupported) {
state.copy(pendingTransactions = pendingTxs())
} else {
state
}
}
}

View file

@ -1,100 +0,0 @@
package com.tangem.features.txhistory.utils
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.pagination.Batch
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.annotations.RemoveWithToggle
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]. Renders pre-redesign tx-history UI.")
@RemoveWithToggle("APP_REDESIGN_ENABLED")
internal class TxHistoryLegacyUiManager(
private val state: MutableStateFlow<TxHistoryListState>,
private val txHistoryItemConverter: TxHistoryItemToTransactionStateConverter,
private val txHistoryUiActions: TxHistoryUiActions,
) {
@OptIn(ExperimentalCoroutinesApi::class)
val items: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = state
.filter { state ->
state.status !is PaginationStatus.None &&
state.status !is PaginationStatus.InitialLoading &&
state.status !is PaginationStatus.InitialLoadingError
}
.mapLatest { state ->
state.legacyUiBatches.asSequence()
.flatMap { it.data }
.toImmutableList()
}
.distinctUntilChanged()
fun createOrUpdateUiBatches(
newCurrencyBatches: List<Batch<Int, PaginationWrapper<TxInfo>>>,
shouldClearUiBatches: Boolean,
): List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> {
val currentUiBatches = state.value.legacyUiBatches
val batches = if (shouldClearUiBatches) mutableListOf() else currentUiBatches.toMutableList()
for ((key, data) in newCurrencyBatches) {
val existingBatchIndex = batches.indexOfFirst { it.key == key }
if (existingBatchIndex == -1) {
val items = generateUiItems(key, data)
batches.add(Batch(key = key, data = items))
} else if (currentUiBatches[existingBatchIndex].data.transactionItemsSizeNotEqual(data.items)) {
val items = generateUiItems(key, data)
batches[existingBatchIndex] = Batch(key = key, data = items)
}
}
return batches
}
private fun generateUiItems(key: Int, data: PaginationWrapper<TxInfo>): List<TxHistoryUM.TxHistoryItemUM> {
val items = mutableListOf<TxHistoryUM.TxHistoryItemUM>()
if (key == 0) {
items.add(TxHistoryUM.TxHistoryItemUM.Title(onExploreClick = txHistoryUiActions::openExplorer))
}
if (data.items.isNotEmpty()) {
val firstItem = data.items.first()
val firstDate = firstItem.timestampInMillis.toDateFormatWithTodayYesterday()
items.add(
TxHistoryUM.TxHistoryItemUM.GroupTitle(
title = firstDate,
itemKey = "$key-$firstDate",
),
)
items.add(TxHistoryUM.TxHistoryItemUM.Transaction(txHistoryItemConverter.convert(firstItem)))
data.items.zipWithNext { current, next ->
val currentDate = current.timestampInMillis.toDateFormatWithTodayYesterday()
val nextDate = next.timestampInMillis.toDateFormatWithTodayYesterday()
if (currentDate != nextDate) {
items.add(
TxHistoryUM.TxHistoryItemUM.GroupTitle(
title = nextDate,
itemKey = "$key-$nextDate",
),
)
}
items.add(TxHistoryUM.TxHistoryItemUM.Transaction(txHistoryItemConverter.convert(next)))
}
}
return items
}
private fun List<TxHistoryUM.TxHistoryItemUM>.transactionItemsSizeNotEqual(txInfos: List<TxInfo>): Boolean {
return this.filterIsInstance<TxHistoryUM.TxHistoryItemUM.Transaction>().size != txInfos.size
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.features.txhistory.utils
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.wallet.UserWalletId
@ -9,7 +8,6 @@ import com.tangem.domain.txhistory.model.TxHistoryListConfig
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionItemUMConverter
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
import com.tangem.features.txhistory.model.TxHistoryLookupContext
import com.tangem.features.txhistory.state.TxHistoryItemsSnapshot
import com.tangem.pagination.BatchAction
@ -34,10 +32,8 @@ internal class TxHistoryListManager(
private val dispatchers: CoroutineDispatcherProvider,
private val userWalletId: UserWalletId,
private val currency: CryptoCurrency,
private val designFeatureToggles: DesignFeatureToggles,
private val txHistoryUiActions: TxHistoryUiActions,
private val lookupDataFlow: Flow<TxHistoryLookupContext>,
legacyTxHistoryItemConverter: TxHistoryItemToTransactionStateConverter,
) {
private val jobHolder = JobHolder()
@ -48,17 +44,8 @@ internal class TxHistoryListManager(
)
private val state: MutableStateFlow<TxHistoryListState> = MutableStateFlow(TxHistoryListState())
private val uiManager = TxHistoryUiManager(state = state)
private val legacyUiManager = TxHistoryLegacyUiManager(
state = state,
txHistoryItemConverter = legacyTxHistoryItemConverter,
txHistoryUiActions = txHistoryUiActions,
)
val uiItems: Flow<TxHistoryItemsSnapshot> = if (designFeatureToggles.isRedesignEnabled) {
uiManager.items.map(TxHistoryItemsSnapshot::Items)
} else {
legacyUiManager.items.map(TxHistoryItemsSnapshot::LegacyItems)
}
val uiItems: Flow<TxHistoryItemsSnapshot> = uiManager.items.map(TxHistoryItemsSnapshot::Items)
val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged()
suspend fun init() = coroutineScope {
@ -76,24 +63,16 @@ internal class TxHistoryListManager(
.launchIn(scope = this)
.saveIn(autoLoadMoreJobHolder)
if (designFeatureToggles.isRedesignEnabled) {
var previousLookup: TxHistoryLookupContext? = null
combine(batchFlow.state, lookupDataFlow) { batchState, lookup -> batchState to lookup }
.onEach { (batchState, lookup) ->
val isLookupChanged = previousLookup != null && previousLookup != lookup
previousLookup = lookup
updateState(batchState, lookup, isLookupChanged)
}
.flowOn(dispatchers.default)
.launchIn(scope = this)
.saveIn(jobHolder)
} else {
batchFlow.state
.onEach { batchState -> updateState(batchState, lookupContext = null, isLookupChanged = false) }
.flowOn(dispatchers.default)
.launchIn(scope = this)
.saveIn(jobHolder)
}
var previousLookup: TxHistoryLookupContext? = null
combine(batchFlow.state, lookupDataFlow) { batchState, lookup -> batchState to lookup }
.onEach { (batchState, lookup) ->
val isLookupChanged = previousLookup != null && previousLookup != lookup
previousLookup = lookup
updateState(batchState, lookup, isLookupChanged)
}
.flowOn(dispatchers.default)
.launchIn(scope = this)
.saveIn(jobHolder)
}
suspend fun startLoading() {
@ -129,32 +108,19 @@ internal class TxHistoryListManager(
val isInitialToPaginating = state.status is PaginationStatus.InitialLoading &&
batchListState.status is PaginationStatus.Paginating
val shouldClearUiBatches = isInitialToPaginating || isLookupChanged
val isRedesignEnabled = designFeatureToggles.isRedesignEnabled
val converter = TxHistoryItemToTransactionItemUMConverter(
currency = currency,
txHistoryUiActions = txHistoryUiActions,
lookupContext = lookupContext,
)
state.copy(
status = batchListState.status,
rawBatches = batchListState.data,
uiBatches = if (isRedesignEnabled) {
val converter = TxHistoryItemToTransactionItemUMConverter(
currency = currency,
txHistoryUiActions = txHistoryUiActions,
lookupContext = lookupContext,
)
uiManager.createOrUpdateUiBatches(
newCurrencyBatches = batchListState.data,
shouldClearUiBatches = shouldClearUiBatches,
converter = converter,
)
} else {
state.uiBatches
},
legacyUiBatches = if (isRedesignEnabled) {
state.legacyUiBatches
} else {
legacyUiManager.createOrUpdateUiBatches(
newCurrencyBatches = batchListState.data,
shouldClearUiBatches = shouldClearUiBatches,
)
},
uiBatches = uiManager.createOrUpdateUiBatches(
newCurrencyBatches = batchListState.data,
shouldClearUiBatches = shouldClearUiBatches,
converter = converter,
),
)
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.features.txhistory.utils
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.pagination.Batch
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.annotations.RemoveWithToggle
@ -14,5 +13,4 @@ internal data class TxHistoryListState(
val status: PaginationStatus<*> = PaginationStatus.None,
val rawBatches: List<Batch<Int, PaginationWrapper<TxInfo>>> = emptyList(),
val uiBatches: List<Batch<Int, List<TxHistoryItemsUM.TxHistoryItemUM>>> = emptyList(),
val legacyUiBatches: List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> = emptyList(),
)

View file

@ -2,11 +2,7 @@ package com.tangem.features.txhistory.state
import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM
import io.mockk.every
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@ -14,12 +10,7 @@ import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class TxHistoryStateControllerTest {
private val controller = TxHistoryStateController(
designFeatureToggles = mockk { every { isRedesignEnabled } returns true },
)
private val legacyController = TxHistoryStateController(
designFeatureToggles = mockk { every { isRedesignEnabled } returns false },
)
private val controller = TxHistoryStateController()
@Test
fun `GIVEN empty items snapshot WHEN setContent THEN Empty state with explorer action`() {
@ -79,41 +70,4 @@ internal class TxHistoryStateControllerTest {
assertThat(controller.uiState.value).isInstanceOf(TxHistoryItemsUM.Empty::class.java)
}
// region Legacy (e.g. Solana: probe reports HasTransactions but the mapped page is empty)
@Test
fun `GIVEN legacy snapshot with only a title WHEN setContent THEN legacy Empty state with explorer`() {
val onExploreClick = {}
legacyController.setContent(
snapshot = TxHistoryItemsSnapshot.LegacyItems(
persistentListOf(TxHistoryUM.TxHistoryItemUM.Title(onExploreClick = {})),
),
loadMore = { true },
onExploreClick = onExploreClick,
)
val state = legacyController.legacyUiState.value
assertThat(state).isInstanceOf(TxHistoryUM.Empty::class.java)
assertThat((state as TxHistoryUM.Empty).onExploreClick).isEqualTo(onExploreClick)
}
@Test
fun `GIVEN legacy snapshot with transactions WHEN setContent THEN legacy Content state`() {
legacyController.setContent(
snapshot = TxHistoryItemsSnapshot.LegacyItems(
persistentListOf(
TxHistoryUM.TxHistoryItemUM.Title(onExploreClick = {}),
TxHistoryUM.TxHistoryItemUM.Transaction(TransactionState.Loading("hash")),
),
),
loadMore = { true },
onExploreClick = {},
)
assertThat(legacyController.legacyUiState.value).isInstanceOf(TxHistoryUM.Content::class.java)
}
// endregion
}

View file

@ -12,14 +12,12 @@ import com.tangem.domain.txhistory.models.Page
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.repository.ExpressHistoryPage
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.BatchListSource
import com.tangem.pagination.PaginationStatus
import com.tangem.pagination.fetcher.BatchFetcher
import com.tangem.pagination.toBatchFlow
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -152,10 +150,8 @@ internal class TxHistoryListManagerTest {
dispatchers = repository.dispatchers,
userWalletId = userWalletId,
currency = currency,
designFeatureToggles = mockk { every { isRedesignEnabled } returns false },
txHistoryUiActions = mockk(relaxed = true),
lookupDataFlow = emptyFlow(),
legacyTxHistoryItemConverter = mockk<TxHistoryItemToTransactionStateConverter>(relaxed = true),
)
private fun page(itemCount: Int, isLast: Boolean): Page2Spec = Page2Spec(itemCount = itemCount, isLast = isLast)