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

View file

@ -1,17 +1,12 @@
package com.tangem.features.txhistory.state package com.tangem.features.txhistory.state
import com.tangem.features.txhistory.entity.TxHistoryItemsUM import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
/** /**
* Snapshot of transaction history items emitted by [TxHistoryListManager]. Wraps either the * Snapshot of transaction history items emitted by [TxHistoryListManager].
* primary or legacy item list so that one [Flow] can carry both pipelines, with the active
* variant chosen via the design feature toggle.
*/ */
internal sealed interface TxHistoryItemsSnapshot { internal sealed interface TxHistoryItemsSnapshot {
data class Items(val items: ImmutableList<TxHistoryItemsUM.TxHistoryItemUM>) : 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 package com.tangem.features.txhistory.state
import com.tangem.core.decompose.di.ModelScoped 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.TransactionItemUM
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.features.txhistory.entity.TxHistoryItemsUM import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM import com.tangem.features.txhistory.entity.TxHistoryUM
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import javax.inject.Inject import javax.inject.Inject
/** /**
* Owns the transaction history UI state and routes updates to either [legacyUiState] or * Owns the transaction history UI state and routes updates to [uiState].
* [uiState] based on [DesignFeatureToggles.isRedesignEnabled]. Only the active pipeline gets
* emitted to; the inactive flow stays at its initial Loading value.
*/ */
@ModelScoped @ModelScoped
internal class TxHistoryStateController @Inject constructor( internal class TxHistoryStateController @Inject constructor() {
private val designFeatureToggles: DesignFeatureToggles,
) {
private val _legacyUiState: MutableStateFlow<TxHistoryUM> = /**
MutableStateFlow(TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = {})) * Pre-redesign state. Kept only to satisfy consumers that still read the legacy UI (they are
val legacyUiState: StateFlow<TxHistoryUM> = _legacyUiState * 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> = private val _uiState: MutableStateFlow<TxHistoryItemsUM> =
MutableStateFlow(TxHistoryItemsUM.Loading(isBalanceHidden = true, onExploreClick = {})) MutableStateFlow(TxHistoryItemsUM.Loading(isBalanceHidden = true, onExploreClick = {}))
val uiState: StateFlow<TxHistoryItemsUM> = _uiState val uiState: StateFlow<TxHistoryItemsUM> = _uiState
val isNotSupported: Boolean val isNotSupported: Boolean
get() = if (designFeatureToggles.isRedesignEnabled) { get() = _uiState.value is TxHistoryItemsUM.NotSupported
_uiState.value is TxHistoryItemsUM.NotSupported
} else {
_legacyUiState.value is TxHistoryUM.NotSupported
}
fun setLoading(isBalanceHidden: Boolean, onExploreClick: () -> Unit) { fun setLoading(isBalanceHidden: Boolean, onExploreClick: () -> Unit) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.value = TxHistoryItemsUM.Loading( _uiState.value = TxHistoryItemsUM.Loading(
isBalanceHidden = isBalanceHidden, isBalanceHidden = isBalanceHidden,
onExploreClick = onExploreClick, onExploreClick = onExploreClick,
) )
} else {
_legacyUiState.value = TxHistoryUM.Loading(
isBalanceHidden = isBalanceHidden,
onExploreClick = onExploreClick,
)
}
} }
fun setLoadingIfNotContent(onExploreClick: () -> Unit) { fun setLoadingIfNotContent(onExploreClick: () -> Unit) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.update { state -> _uiState.update { state ->
state as? TxHistoryItemsUM.Content ?: TxHistoryItemsUM.Loading(state.isBalanceHidden, onExploreClick) state as? TxHistoryItemsUM.Content ?: TxHistoryItemsUM.Loading(state.isBalanceHidden, onExploreClick)
} }
} else {
_legacyUiState.update { state ->
state as? TxHistoryUM.Content ?: TxHistoryUM.Loading(state.isBalanceHidden, onExploreClick)
}
}
} }
fun setError(onReloadClick: () -> Unit, onExploreClick: () -> Unit) { fun setError(onReloadClick: () -> Unit, onExploreClick: () -> Unit) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.value = TxHistoryItemsUM.Error( _uiState.value = TxHistoryItemsUM.Error(
isBalanceHidden = _uiState.value.isBalanceHidden, isBalanceHidden = _uiState.value.isBalanceHidden,
onReloadClick = onReloadClick, onReloadClick = onReloadClick,
onExploreClick = onExploreClick, onExploreClick = onExploreClick,
) )
} else {
_legacyUiState.value = TxHistoryUM.Error(
isBalanceHidden = _legacyUiState.value.isBalanceHidden,
onReloadClick = onReloadClick,
onExploreClick = onExploreClick,
)
}
} }
fun setEmpty(onExploreClick: () -> Unit) { fun setEmpty(onExploreClick: () -> Unit) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.value = TxHistoryItemsUM.Empty( _uiState.value = TxHistoryItemsUM.Empty(
isBalanceHidden = _uiState.value.isBalanceHidden, isBalanceHidden = _uiState.value.isBalanceHidden,
onExploreClick = onExploreClick, onExploreClick = onExploreClick,
) )
} else {
_legacyUiState.value = TxHistoryUM.Empty(
isBalanceHidden = _legacyUiState.value.isBalanceHidden,
onExploreClick = onExploreClick,
)
}
} }
fun setNotSupported(onExploreClick: () -> Unit) { fun setNotSupported(onExploreClick: () -> Unit) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.value = TxHistoryItemsUM.NotSupported( _uiState.value = TxHistoryItemsUM.NotSupported(
isBalanceHidden = _uiState.value.isBalanceHidden, isBalanceHidden = _uiState.value.isBalanceHidden,
pendingTransactions = persistentListOf(), pendingTransactions = persistentListOf(),
onExploreClick = onExploreClick, onExploreClick = onExploreClick,
) )
} else {
_legacyUiState.value = TxHistoryUM.NotSupported(
isBalanceHidden = _legacyUiState.value.isBalanceHidden,
pendingTransactions = persistentListOf(),
onExploreClick = onExploreClick,
)
}
} }
fun setContent(snapshot: TxHistoryItemsSnapshot, loadMore: () -> Boolean, onExploreClick: () -> Unit) { 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) { fun updateLoadingMore(isLoadingMore: Boolean) {
if (!designFeatureToggles.isRedesignEnabled) return
_uiState.update { state -> _uiState.update { state ->
if (state is TxHistoryItemsUM.Content && state.isLoadingMore != isLoadingMore) { if (state is TxHistoryItemsUM.Content && state.isLoadingMore != isLoadingMore) {
state.copy(isLoadingMore = isLoadingMore) state.copy(isLoadingMore = isLoadingMore)
@ -160,18 +101,10 @@ internal class TxHistoryStateController @Inject constructor(
} }
fun updateBalanceHidden(isBalanceHidden: Boolean) { fun updateBalanceHidden(isBalanceHidden: Boolean) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.update { state -> state.copySealed(isBalanceHidden = isBalanceHidden) } _uiState.update { state -> state.copySealed(isBalanceHidden = isBalanceHidden) }
} else {
_legacyUiState.update { state -> state.copySealed(isBalanceHidden = isBalanceHidden) }
}
} }
fun updatePendingTransactions( fun updatePendingTransactions(pendingTxs: () -> ImmutableList<TransactionItemUM>) {
pendingTxs: () -> ImmutableList<TransactionItemUM>,
legacyPendingTxs: () -> ImmutableList<TransactionState>,
) {
if (designFeatureToggles.isRedesignEnabled) {
_uiState.update { state -> _uiState.update { state ->
if (state is TxHistoryItemsUM.NotSupported) { if (state is TxHistoryItemsUM.NotSupported) {
state.copy(pendingTransactions = pendingTxs()) state.copy(pendingTransactions = pendingTxs())
@ -179,14 +112,5 @@ internal class TxHistoryStateController @Inject constructor(
state state
} }
} }
} else {
_legacyUiState.update { state ->
if (state is TxHistoryUM.NotSupported) {
state.copy(pendingTransactions = legacyPendingTxs())
} 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 package com.tangem.features.txhistory.utils
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.wallet.UserWalletId 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.models.PaginationWrapper
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2 import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionItemUMConverter 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.model.TxHistoryLookupContext
import com.tangem.features.txhistory.state.TxHistoryItemsSnapshot import com.tangem.features.txhistory.state.TxHistoryItemsSnapshot
import com.tangem.pagination.BatchAction import com.tangem.pagination.BatchAction
@ -34,10 +32,8 @@ internal class TxHistoryListManager(
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val userWalletId: UserWalletId, private val userWalletId: UserWalletId,
private val currency: CryptoCurrency, private val currency: CryptoCurrency,
private val designFeatureToggles: DesignFeatureToggles,
private val txHistoryUiActions: TxHistoryUiActions, private val txHistoryUiActions: TxHistoryUiActions,
private val lookupDataFlow: Flow<TxHistoryLookupContext>, private val lookupDataFlow: Flow<TxHistoryLookupContext>,
legacyTxHistoryItemConverter: TxHistoryItemToTransactionStateConverter,
) { ) {
private val jobHolder = JobHolder() private val jobHolder = JobHolder()
@ -48,17 +44,8 @@ internal class TxHistoryListManager(
) )
private val state: MutableStateFlow<TxHistoryListState> = MutableStateFlow(TxHistoryListState()) private val state: MutableStateFlow<TxHistoryListState> = MutableStateFlow(TxHistoryListState())
private val uiManager = TxHistoryUiManager(state = state) private val uiManager = TxHistoryUiManager(state = state)
private val legacyUiManager = TxHistoryLegacyUiManager(
state = state,
txHistoryItemConverter = legacyTxHistoryItemConverter,
txHistoryUiActions = txHistoryUiActions,
)
val uiItems: Flow<TxHistoryItemsSnapshot> = if (designFeatureToggles.isRedesignEnabled) { val uiItems: Flow<TxHistoryItemsSnapshot> = uiManager.items.map(TxHistoryItemsSnapshot::Items)
uiManager.items.map(TxHistoryItemsSnapshot::Items)
} else {
legacyUiManager.items.map(TxHistoryItemsSnapshot::LegacyItems)
}
val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged() val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged()
suspend fun init() = coroutineScope { suspend fun init() = coroutineScope {
@ -76,7 +63,6 @@ internal class TxHistoryListManager(
.launchIn(scope = this) .launchIn(scope = this)
.saveIn(autoLoadMoreJobHolder) .saveIn(autoLoadMoreJobHolder)
if (designFeatureToggles.isRedesignEnabled) {
var previousLookup: TxHistoryLookupContext? = null var previousLookup: TxHistoryLookupContext? = null
combine(batchFlow.state, lookupDataFlow) { batchState, lookup -> batchState to lookup } combine(batchFlow.state, lookupDataFlow) { batchState, lookup -> batchState to lookup }
.onEach { (batchState, lookup) -> .onEach { (batchState, lookup) ->
@ -87,13 +73,6 @@ internal class TxHistoryListManager(
.flowOn(dispatchers.default) .flowOn(dispatchers.default)
.launchIn(scope = this) .launchIn(scope = this)
.saveIn(jobHolder) .saveIn(jobHolder)
} else {
batchFlow.state
.onEach { batchState -> updateState(batchState, lookupContext = null, isLookupChanged = false) }
.flowOn(dispatchers.default)
.launchIn(scope = this)
.saveIn(jobHolder)
}
} }
suspend fun startLoading() { suspend fun startLoading() {
@ -129,32 +108,19 @@ internal class TxHistoryListManager(
val isInitialToPaginating = state.status is PaginationStatus.InitialLoading && val isInitialToPaginating = state.status is PaginationStatus.InitialLoading &&
batchListState.status is PaginationStatus.Paginating batchListState.status is PaginationStatus.Paginating
val shouldClearUiBatches = isInitialToPaginating || isLookupChanged val shouldClearUiBatches = isInitialToPaginating || isLookupChanged
val isRedesignEnabled = designFeatureToggles.isRedesignEnabled
state.copy(
status = batchListState.status,
rawBatches = batchListState.data,
uiBatches = if (isRedesignEnabled) {
val converter = TxHistoryItemToTransactionItemUMConverter( val converter = TxHistoryItemToTransactionItemUMConverter(
currency = currency, currency = currency,
txHistoryUiActions = txHistoryUiActions, txHistoryUiActions = txHistoryUiActions,
lookupContext = lookupContext, lookupContext = lookupContext,
) )
uiManager.createOrUpdateUiBatches( state.copy(
status = batchListState.status,
rawBatches = batchListState.data,
uiBatches = uiManager.createOrUpdateUiBatches(
newCurrencyBatches = batchListState.data, newCurrencyBatches = batchListState.data,
shouldClearUiBatches = shouldClearUiBatches, shouldClearUiBatches = shouldClearUiBatches,
converter = converter, converter = converter,
) ),
} else {
state.uiBatches
},
legacyUiBatches = if (isRedesignEnabled) {
state.legacyUiBatches
} else {
legacyUiManager.createOrUpdateUiBatches(
newCurrencyBatches = batchListState.data,
shouldClearUiBatches = shouldClearUiBatches,
)
},
) )
} }
} }

View file

@ -3,7 +3,6 @@ package com.tangem.features.txhistory.utils
import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.features.txhistory.entity.TxHistoryItemsUM import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.pagination.Batch import com.tangem.pagination.Batch
import com.tangem.pagination.PaginationStatus import com.tangem.pagination.PaginationStatus
import com.tangem.utils.annotations.RemoveWithToggle import com.tangem.utils.annotations.RemoveWithToggle
@ -14,5 +13,4 @@ internal data class TxHistoryListState(
val status: PaginationStatus<*> = PaginationStatus.None, val status: PaginationStatus<*> = PaginationStatus.None,
val rawBatches: List<Batch<Int, PaginationWrapper<TxInfo>>> = emptyList(), val rawBatches: List<Batch<Int, PaginationWrapper<TxInfo>>> = emptyList(),
val uiBatches: List<Batch<Int, List<TxHistoryItemsUM.TxHistoryItemUM>>> = 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.google.common.truth.Truth.assertThat
import com.tangem.core.ui.components.transactions.state.TransactionItemUM 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.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM
import io.mockk.every
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
@ -14,12 +10,7 @@ import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class TxHistoryStateControllerTest { internal class TxHistoryStateControllerTest {
private val controller = TxHistoryStateController( private val controller = TxHistoryStateController()
designFeatureToggles = mockk { every { isRedesignEnabled } returns true },
)
private val legacyController = TxHistoryStateController(
designFeatureToggles = mockk { every { isRedesignEnabled } returns false },
)
@Test @Test
fun `GIVEN empty items snapshot WHEN setContent THEN Empty state with explorer action`() { 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) 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.models.PaginationWrapper
import com.tangem.domain.txhistory.repository.ExpressHistoryPage import com.tangem.domain.txhistory.repository.ExpressHistoryPage
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2 import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.BatchListSource import com.tangem.pagination.BatchListSource
import com.tangem.pagination.PaginationStatus import com.tangem.pagination.PaginationStatus
import com.tangem.pagination.fetcher.BatchFetcher import com.tangem.pagination.fetcher.BatchFetcher
import com.tangem.pagination.toBatchFlow import com.tangem.pagination.toBatchFlow
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -152,10 +150,8 @@ internal class TxHistoryListManagerTest {
dispatchers = repository.dispatchers, dispatchers = repository.dispatchers,
userWalletId = userWalletId, userWalletId = userWalletId,
currency = currency, currency = currency,
designFeatureToggles = mockk { every { isRedesignEnabled } returns false },
txHistoryUiActions = mockk(relaxed = true), txHistoryUiActions = mockk(relaxed = true),
lookupDataFlow = emptyFlow(), lookupDataFlow = emptyFlow(),
legacyTxHistoryItemConverter = mockk<TxHistoryItemToTransactionStateConverter>(relaxed = true),
) )
private fun page(itemCount: Int, isLast: Boolean): Page2Spec = Page2Spec(itemCount = itemCount, isLast = isLast) private fun page(itemCount: Int, isLast: Boolean): Page2Spec = Page2Spec(itemCount = itemCount, isLast = isLast)