Updated on 2026-08-14
This commit is contained in:
parent
1437c14019
commit
3f671e3e0f
46 changed files with 1454 additions and 43 deletions
|
|
@ -186,6 +186,8 @@ dependencies {
|
|||
implementation(projects.features.onboardingV2.impl)
|
||||
implementation(projects.features.stories.api)
|
||||
implementation(projects.features.stories.impl)
|
||||
implementation(projects.features.txhistory.api)
|
||||
implementation(projects.features.txhistory.impl)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
|
|
|
|||
|
|
@ -54,5 +54,9 @@
|
|||
{
|
||||
"name": "STAKING_CARDANO_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TX_HISTORY_REFACTORING_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
batchFetcher.fetchNext(action.requestParams, lastResult)
|
||||
}.getOrElse { BatchFetchResult.Error(it) }
|
||||
|
||||
lastRequestResult.value = lastResult
|
||||
lastRequestResult.value = res
|
||||
|
||||
state.update { currentState ->
|
||||
when (res) {
|
||||
|
|
|
|||
|
|
@ -445,5 +445,6 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider<
|
|||
onClick = {},
|
||||
),
|
||||
TransactionState.Loading(txHash = UUID.randomUUID().toString()),
|
||||
TransactionState.Locked(txHash = UUID.randomUUID().toString()),
|
||||
),
|
||||
)
|
||||
|
|
@ -104,7 +104,7 @@ private fun LazyListScope.contentItems(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun PendingTxsBlock(pendingTxs: ImmutableList<TransactionState>, isBalanceHidden: Boolean) {
|
||||
fun PendingTxsBlock(pendingTxs: ImmutableList<TransactionState>, isBalanceHidden: Boolean) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
* @param modifier modifier
|
||||
*/
|
||||
@Composable
|
||||
internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ dependencies {
|
|||
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.pagination)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package com.tangem.data.txhistory.di
|
|||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository
|
||||
import com.tangem.data.txhistory.repository.RefactoredTxHistoryRepository
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -32,4 +34,18 @@ internal object TxHistoryDataModule {
|
|||
txHistoryItemsStore,
|
||||
dispatchers,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTxHistoryRepositoryV2(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
txHistoryItemsStore: TxHistoryItemsStore,
|
||||
cacheRegistry: CacheRegistry,
|
||||
): TxHistoryRepositoryV2 = RefactoredTxHistoryRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = dispatchers,
|
||||
txHistoryItemsStore = txHistoryItemsStore,
|
||||
cacheRegistry = cacheRegistry,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package com.tangem.data.txhistory.repository
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.txhistory.repository.paging.TxHistoryPageBatchFetcher
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListBatchFlow
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListBatchingContext
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListConfig
|
||||
import com.tangem.domain.txhistory.models.Page
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.walletmanager.utils.SdkPageConverter
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.BatchListSource
|
||||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import timber.log.Timber
|
||||
|
||||
internal class RefactoredTxHistoryRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val txHistoryItemsStore: TxHistoryItemsStore,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : TxHistoryRepositoryV2 {
|
||||
|
||||
private val sdkPageConverter = SdkPageConverter()
|
||||
private val TxHistoryListConfig.storeKey get() = TxHistoryItemsStore.Key(userWalletId, currency)
|
||||
|
||||
override fun getTxHistoryBatchFlow(batchSize: Int, context: TxHistoryListBatchingContext): TxHistoryListBatchFlow {
|
||||
return BatchListSource(
|
||||
fetchDispatcher = dispatchers.io,
|
||||
context = context,
|
||||
generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: 0 },
|
||||
batchFetcher = createFetcher(batchSize),
|
||||
).toBatchFlow()
|
||||
}
|
||||
|
||||
private fun createFetcher(
|
||||
batchSize: Int,
|
||||
): TxHistoryPageBatchFetcher<TxHistoryListConfig, PaginationWrapper<TxHistoryItem>> =
|
||||
TxHistoryPageBatchFetcher { request, _ ->
|
||||
val wrappedItems = loadItems(request, batchSize)
|
||||
BatchFetchResult.Success(
|
||||
data = wrappedItems,
|
||||
empty = wrappedItems.items.isEmpty(),
|
||||
last = wrappedItems.nextPage is Page.LastPage,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadItems(
|
||||
request: TxHistoryPageBatchFetcher.Request<TxHistoryListConfig>,
|
||||
batchSize: Int,
|
||||
): PaginationWrapper<TxHistoryItem> {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getTxHistoryPageKey(request.page, request.params),
|
||||
skipCache = request.params.refresh,
|
||||
block = { fetch(request, batchSize) },
|
||||
)
|
||||
|
||||
return txHistoryItemsStore.getSync(request.page, request.params)
|
||||
}
|
||||
|
||||
private suspend fun fetch(request: TxHistoryPageBatchFetcher.Request<TxHistoryListConfig>, batchSize: Int) {
|
||||
val wrappedItems = walletManagersFacade.getTxHistoryItems(
|
||||
userWalletId = request.params.userWalletId,
|
||||
currency = request.params.currency,
|
||||
page = sdkPageConverter.convertBack(request.page),
|
||||
pageSize = batchSize,
|
||||
)
|
||||
|
||||
txHistoryItemsStore.store(key = request.params.storeKey, value = wrappedItems)
|
||||
}
|
||||
|
||||
private suspend fun TxHistoryItemsStore.getSync(
|
||||
pageToLoad: Page,
|
||||
config: TxHistoryListConfig,
|
||||
): PaginationWrapper<TxHistoryItem> {
|
||||
val storedItems = requireNotNull(getSyncOrNull(config.storeKey, pageToLoad)) {
|
||||
"The transaction history page #$pageToLoad could not be retrieved"
|
||||
}
|
||||
|
||||
return if (pageToLoad is Page.Initial) storedItems.addRecentTransactions(config) else storedItems
|
||||
}
|
||||
|
||||
private suspend fun PaginationWrapper<TxHistoryItem>.addRecentTransactions(
|
||||
config: TxHistoryListConfig,
|
||||
): PaginationWrapper<TxHistoryItem> {
|
||||
val recentItems = walletManagersFacade.getRecentTransactions(
|
||||
userWalletId = config.userWalletId,
|
||||
currency = config.currency,
|
||||
)
|
||||
.filterUnconfirmedTransaction()
|
||||
.sortedByDescending { it.timestampInMillis }
|
||||
.filterIfTxAlreadyAdded(apiItems = items)
|
||||
|
||||
return if (recentItems.isEmpty()) {
|
||||
Timber.d("Nothing to add to TxHistory")
|
||||
this
|
||||
} else {
|
||||
Timber.d(
|
||||
"Recent transactions were added to TxHistory: %s",
|
||||
recentItems.joinToString(
|
||||
prefix = "[",
|
||||
postfix = "]",
|
||||
transform = TxHistoryItem::txHash,
|
||||
),
|
||||
)
|
||||
|
||||
return copy(items = recentItems + items)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<TxHistoryItem>.filterUnconfirmedTransaction(): List<TxHistoryItem> {
|
||||
return filter { it.status == TxHistoryItem.TransactionStatus.Unconfirmed }
|
||||
}
|
||||
|
||||
private fun List<TxHistoryItem>.filterIfTxAlreadyAdded(apiItems: List<TxHistoryItem>): List<TxHistoryItem> {
|
||||
return filter { item -> apiItems.none { it.txHash == item.txHash } }
|
||||
}
|
||||
|
||||
private fun getTxHistoryPageKey(page: Page, config: TxHistoryListConfig): String {
|
||||
return "tx_history_page_${config.currency}_${config.userWalletId}_$page"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.data.txhistory.repository.paging
|
||||
|
||||
import com.tangem.domain.txhistory.models.Page
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.exception.EndOfPaginationException
|
||||
import com.tangem.pagination.fetcher.BatchFetcher
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class TxHistoryPageBatchFetcher<TRequestParams : Any, TData : PaginationWrapper<TxHistoryItem>>(
|
||||
private val subFetcher: SubFetcher<TRequestParams, TData>,
|
||||
) : BatchFetcher<TRequestParams, TData> {
|
||||
data class Request<TRequestParams>(val page: Page, val params: TRequestParams)
|
||||
fun interface SubFetcher<TRequestParams : Any, TData> {
|
||||
suspend fun fetch(
|
||||
request: Request<TRequestParams>,
|
||||
lastResult: BatchFetchResult<TData>?,
|
||||
): BatchFetchResult<TData>
|
||||
}
|
||||
|
||||
private val lastRequest = MutableStateFlow<Request<TRequestParams>?>(null)
|
||||
|
||||
override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult<TData> {
|
||||
val req = Request(
|
||||
page = Page.Initial,
|
||||
params = requestParams,
|
||||
)
|
||||
|
||||
val res = runCatching {
|
||||
subFetcher.fetch(request = req, lastResult = null)
|
||||
}.getOrElse {
|
||||
currentCoroutineContext().ensureActive()
|
||||
BatchFetchResult.Error(it)
|
||||
}
|
||||
|
||||
lastRequest.value = req
|
||||
return res
|
||||
}
|
||||
|
||||
override suspend fun fetchNext(
|
||||
overrideRequestParams: TRequestParams?,
|
||||
lastResult: BatchFetchResult<TData>,
|
||||
): BatchFetchResult<TData> {
|
||||
val last = lastRequest.value
|
||||
requireNotNull(last)
|
||||
|
||||
val req = if (lastResult is BatchFetchResult.Success) {
|
||||
if (lastResult.last && overrideRequestParams == null) {
|
||||
return BatchFetchResult.Error(EndOfPaginationException())
|
||||
}
|
||||
|
||||
Request(
|
||||
page = lastResult.data.nextPage,
|
||||
params = overrideRequestParams ?: last.params,
|
||||
)
|
||||
} else {
|
||||
last
|
||||
}
|
||||
|
||||
val res = runCatching {
|
||||
subFetcher.fetch(request = req, lastResult = lastResult)
|
||||
}.getOrElse {
|
||||
currentCoroutineContext().ensureActive()
|
||||
BatchFetchResult.Error(it)
|
||||
}
|
||||
|
||||
lastRequest.value = req
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
|
@ -20,4 +20,6 @@ dependencies {
|
|||
|
||||
/** Android - Other */
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
||||
api(projects.core.pagination)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.domain.txhistory.models
|
||||
|
||||
sealed class Page {
|
||||
object Initial : Page()
|
||||
data object Initial : Page()
|
||||
data class Next(val value: String) : Page()
|
||||
object LastPage : Page()
|
||||
data object LastPage : Page()
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.txhistory.model
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
data class TxHistoryListConfig(val userWalletId: UserWalletId, val currency: CryptoCurrency, val refresh: Boolean)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.txhistory.model
|
||||
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.pagination.BatchFlow
|
||||
import com.tangem.pagination.BatchingContext
|
||||
|
||||
typealias TxHistoryListBatchingContext = BatchingContext<Int, TxHistoryListConfig, Nothing>
|
||||
|
||||
typealias TxHistoryListBatchFlow = BatchFlow<Int, PaginationWrapper<TxHistoryItem>, Nothing>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.domain.txhistory.repository
|
||||
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListBatchFlow
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListBatchingContext
|
||||
|
||||
interface TxHistoryRepositoryV2 {
|
||||
|
||||
fun getTxHistoryBatchFlow(batchSize: Int, context: TxHistoryListBatchingContext): TxHistoryListBatchFlow
|
||||
}
|
||||
|
|
@ -84,6 +84,7 @@ dependencies {
|
|||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
implementation(projects.features.tokendetails.api)
|
||||
implementation(projects.features.txhistory.api)
|
||||
implementation(projects.features.qrScanning.api)
|
||||
|
||||
/** DI */
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactor
|
|||
import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory
|
||||
import com.tangem.features.send.impl.presentation.state.fee.*
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.RecipientSendFactory
|
||||
import com.tangem.features.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.*
|
||||
|
|
@ -110,6 +112,8 @@ internal class SendModel @Inject constructor(
|
|||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val shareManager: ShareManager,
|
||||
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
|
||||
private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter,
|
||||
@DelayedWork private val coroutineScope: CoroutineScope,
|
||||
private val innerRouter: InnerSendRouter,
|
||||
private val appRouter: AppRouter,
|
||||
|
|
@ -1012,11 +1016,15 @@ internal class SendModel @Inject constructor(
|
|||
)
|
||||
|
||||
txHistoryItemsCountEither.onRight {
|
||||
getTxHistoryItemsUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
refresh = true,
|
||||
)
|
||||
if (txHistoryFeatureToggles.isFeatureEnabled) {
|
||||
txHistoryContentUpdateEmitter.triggerUpdate()
|
||||
} else {
|
||||
getTxHistoryItemsUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ dependencies {
|
|||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.staking.api)
|
||||
implementation(projects.features.txhistory.api)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
|
||||
import com.tangem.utils.coroutines.DelayedWork
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -24,6 +26,8 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
|
|||
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
|
||||
private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase,
|
||||
private val fetchActionsUseCase: FetchActionsUseCase,
|
||||
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
|
||||
private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter,
|
||||
@DelayedWork private val coroutineScope: CoroutineScope,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
|
|
@ -97,11 +101,15 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
|
|||
)
|
||||
|
||||
txHistoryItemsCountEither.onRight {
|
||||
getTxHistoryItemsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
refresh = true,
|
||||
)
|
||||
if (txHistoryFeatureToggles.isFeatureEnabled) {
|
||||
txHistoryContentUpdateEmitter.triggerUpdate()
|
||||
} else {
|
||||
getTxHistoryItemsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,5 +99,6 @@ dependencies {
|
|||
implementation(projects.features.markets.api)
|
||||
implementation(projects.features.onramp.api)
|
||||
implementation(projects.features.swap.api)
|
||||
implementation(projects.features.txhistory.api)
|
||||
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDeta
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
|
||||
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
|
||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -25,10 +27,20 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: TokenDetailsComponent.Params,
|
||||
tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory,
|
||||
txHistoryComponentFactory: TxHistoryComponent.Factory,
|
||||
txHistoryFeatureToggles: TxHistoryFeatureToggles,
|
||||
deepLinksRegistry: DeepLinksRegistry,
|
||||
) : TokenDetailsComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: TokenDetailsModel = getOrCreateModel(params)
|
||||
private val txHistoryComponent = txHistoryComponentFactory.create(
|
||||
context = child("txHistoryComponent"),
|
||||
params = TxHistoryComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
currency = params.currency,
|
||||
openExplorer = { model.onExploreClick() },
|
||||
),
|
||||
).takeIf { txHistoryFeatureToggles.isFeatureEnabled }
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
@ -54,11 +66,11 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
NavigationBar3ButtonsScrim()
|
||||
TokenDetailsScreen(
|
||||
state = state,
|
||||
tokenMarketBlockComponent = tokenMarketBlockComponent,
|
||||
txHistoryComponent = txHistoryComponent,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
|
|||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
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.decompose.ui.UiMessageSender
|
||||
|
|
@ -76,6 +76,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.e
|
|||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.features.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.*
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
|
@ -122,6 +124,8 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private val onrampFeatureToggles: OnrampFeatureToggles,
|
||||
private val shareManager: ShareManager,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
|
||||
private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter,
|
||||
paramsContainer: ParamsContainer,
|
||||
expressStatusFactory: ExpressStatusFactory.Factory,
|
||||
getUserWalletUseCase: GetUserWalletUseCase,
|
||||
|
|
@ -240,7 +244,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private fun updateContent() {
|
||||
subscribeOnCurrencyStatusUpdates()
|
||||
subscribeOnExpressTransactionsUpdates()
|
||||
updateTxHistory(refresh = false, showItemsLoading = true)
|
||||
updateTxHistory(refresh = false, showItemsLoading = true, initialUpdating = true)
|
||||
|
||||
updateStakingInfo()
|
||||
}
|
||||
|
|
@ -362,29 +366,33 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
* @param refresh - invalidate cache and get data from remote
|
||||
* @param showItemsLoading - show loading items placeholder.
|
||||
*/
|
||||
private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean) {
|
||||
modelScope.launch(dispatchers.main) {
|
||||
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
)
|
||||
|
||||
// if countEither is left, handling error state run inside getLoadingTxHistoryState
|
||||
if (showItemsLoading || txHistoryItemsCountEither.isLeft()) {
|
||||
internalUiState.value = stateFactory.getLoadingTxHistoryState(
|
||||
itemsCountEither = txHistoryItemsCountEither,
|
||||
pendingTransactions = internalUiState.value.pendingTxs,
|
||||
)
|
||||
}
|
||||
|
||||
txHistoryItemsCountEither.onRight {
|
||||
val maybeTxHistory = txHistoryItemsUseCase(
|
||||
private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean, initialUpdating: Boolean = false) {
|
||||
if (txHistoryFeatureToggles.isFeatureEnabled && !initialUpdating) {
|
||||
modelScope.launch { txHistoryContentUpdateEmitter.triggerUpdate() }
|
||||
} else {
|
||||
modelScope.launch(dispatchers.main) {
|
||||
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
refresh = refresh,
|
||||
).map { it.cachedIn(modelScope) }
|
||||
)
|
||||
|
||||
internalUiState.value = stateFactory.getLoadedTxHistoryState(maybeTxHistory)
|
||||
// if countEither is left, handling error state run inside getLoadingTxHistoryState
|
||||
if (showItemsLoading || txHistoryItemsCountEither.isLeft()) {
|
||||
internalUiState.value = stateFactory.getLoadingTxHistoryState(
|
||||
itemsCountEither = txHistoryItemsCountEither,
|
||||
pendingTransactions = internalUiState.value.pendingTxs,
|
||||
)
|
||||
}
|
||||
|
||||
txHistoryItemsCountEither.onRight {
|
||||
val maybeTxHistory = txHistoryItemsUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
refresh = refresh,
|
||||
).map { it.cachedIn(modelScope) }
|
||||
|
||||
internalUiState.value = stateFactory.getLoadedTxHistoryState(maybeTxHistory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui
|
|||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
|
||||
import com.tangem.common.ui.expressStatus.expressTransactionsItems
|
||||
|
|
@ -39,11 +41,18 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
|
||||
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
// TODO: Split to blocks [REDACTED_JIRA]
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockComponent: TokenMarketBlockComponent?) {
|
||||
internal fun TokenDetailsScreen(
|
||||
state: TokenDetailsState,
|
||||
tokenMarketBlockComponent: TokenMarketBlockComponent?,
|
||||
txHistoryComponent: TxHistoryComponent?,
|
||||
) {
|
||||
BackHandler(onBack = state.topAppBarConfig.onBackClick)
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
|
|
@ -57,6 +66,8 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
|
|||
} else {
|
||||
null
|
||||
}
|
||||
val listState = rememberLazyListState()
|
||||
val txHistoryComponentState by txHistoryComponent?.txHistoryState?.collectAsStateWithLifecycle()
|
||||
val betweenItemsPadding = TangemTheme.dimens.spacing12
|
||||
val horizontalPadding = TangemTheme.dimens.spacing16
|
||||
val itemModifier = Modifier
|
||||
|
|
@ -69,6 +80,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
|
|||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(
|
||||
bottom = TangemTheme.dimens.spacing16 + bottomBarHeight,
|
||||
),
|
||||
|
|
@ -145,9 +157,12 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
|
|||
)
|
||||
|
||||
txHistoryItems(
|
||||
state = state.txHistoryState,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
listState = listState,
|
||||
txHistoryComponent = txHistoryComponent,
|
||||
txHistoryComponentState = txHistoryComponentState,
|
||||
txHistoryState = state.txHistoryState,
|
||||
txHistoryItems = txHistoryItems,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -170,6 +185,28 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun LazyListScope.txHistoryItems(
|
||||
listState: LazyListState,
|
||||
txHistoryComponent: TxHistoryComponent?,
|
||||
txHistoryComponentState: TxHistoryUM?,
|
||||
txHistoryState: TxHistoryState,
|
||||
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>?,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
if (txHistoryComponent != null && txHistoryComponentState != null) {
|
||||
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryComponentState) }
|
||||
} else {
|
||||
txHistoryItems(
|
||||
state = txHistoryState,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
txHistoryItems = txHistoryItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline operator fun <T> State<T>?.getValue(thisObj: Any?, property: KProperty<*>): T? = this?.value
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
|
|
@ -181,6 +218,7 @@ private fun TokenDetailsScreenPreview(
|
|||
TokenDetailsScreen(
|
||||
state = state,
|
||||
tokenMarketBlockComponent = null,
|
||||
txHistoryComponent = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
features/txhistory/api/.gitignore
vendored
Normal file
1
features/txhistory/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
26
features/txhistory/api/build.gradle.kts
Normal file
26
features/txhistory/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.txhistory.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Core */
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.decompose)
|
||||
|
||||
/** Domain models */
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
implementation(deps.compose.foundation)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.txhistory
|
||||
|
||||
interface TxHistoryFeatureToggles {
|
||||
val isFeatureEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.features.txhistory.component
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Stable
|
||||
interface TxHistoryComponent {
|
||||
|
||||
val txHistoryState: StateFlow<TxHistoryUM>
|
||||
|
||||
fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM)
|
||||
|
||||
fun reload()
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val openExplorer: () -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, TxHistoryComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.txhistory.entity
|
||||
|
||||
interface TxHistoryContentUpdateEmitter {
|
||||
suspend fun triggerUpdate()
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.features.txhistory.entity
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
sealed interface TxHistoryUM {
|
||||
|
||||
val isBalanceHidden: Boolean
|
||||
|
||||
data class Loading(override val isBalanceHidden: Boolean, private val onExploreClick: () -> Unit) : TxHistoryUM {
|
||||
val items = persistentListOf(
|
||||
TxHistoryItemUM.Title(onExploreClick = onExploreClick),
|
||||
TxHistoryItemUM.Transaction(TransactionState.Loading("LOADING_TX_HASH_1")),
|
||||
TxHistoryItemUM.Transaction(TransactionState.Loading("LOADING_TX_HASH_2")),
|
||||
TxHistoryItemUM.Transaction(TransactionState.Loading("LOADING_TX_HASH_3")),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallet transaction history state with content
|
||||
*/
|
||||
data class Content(
|
||||
override val isBalanceHidden: Boolean,
|
||||
val items: ImmutableList<TxHistoryItemUM>,
|
||||
val loadMore: () -> Boolean,
|
||||
) : TxHistoryUM
|
||||
|
||||
/** Empty state */
|
||||
data class Empty(override val isBalanceHidden: Boolean, val onExploreClick: () -> Unit) : TxHistoryUM
|
||||
|
||||
/**
|
||||
* Not supported tx history state
|
||||
*
|
||||
* @property pendingTransactions pending transactions
|
||||
* @property onExploreClick lambda be invoke when explore button was clicked
|
||||
*/
|
||||
data class NotSupported(
|
||||
override val isBalanceHidden: Boolean,
|
||||
val pendingTransactions: ImmutableList<TransactionState>,
|
||||
val onExploreClick: () -> Unit,
|
||||
) : TxHistoryUM
|
||||
|
||||
/**
|
||||
* Error state
|
||||
*
|
||||
* @property onReloadClick lambda be invoke when reload button was clicked
|
||||
*/
|
||||
data class Error(
|
||||
override val isBalanceHidden: Boolean,
|
||||
val onReloadClick: () -> Unit,
|
||||
val onExploreClick: () -> Unit,
|
||||
) : TxHistoryUM
|
||||
|
||||
fun copySealed(isBalanceHidden: Boolean): TxHistoryUM {
|
||||
return when (this) {
|
||||
is Content -> copy(isBalanceHidden = isBalanceHidden)
|
||||
is NotSupported -> copy(isBalanceHidden = isBalanceHidden)
|
||||
is Empty -> copy(isBalanceHidden = isBalanceHidden)
|
||||
is Error -> copy(isBalanceHidden = isBalanceHidden)
|
||||
is Loading -> copy(isBalanceHidden = isBalanceHidden)
|
||||
}
|
||||
}
|
||||
|
||||
/** Transactions history item state */
|
||||
sealed interface TxHistoryItemUM {
|
||||
|
||||
/**
|
||||
* Title item
|
||||
*
|
||||
* @property onExploreClick lambda be invoke when explore button was clicked
|
||||
*/
|
||||
data class Title(val onExploreClick: () -> Unit) : TxHistoryItemUM
|
||||
|
||||
/**
|
||||
* Group title item
|
||||
*
|
||||
* @property title title
|
||||
* @property itemKey key to use in compose
|
||||
*/
|
||||
data class GroupTitle(
|
||||
val title: String,
|
||||
val itemKey: String,
|
||||
) : TxHistoryItemUM
|
||||
|
||||
/**
|
||||
* Transaction item
|
||||
*
|
||||
* @property state transaction state
|
||||
*/
|
||||
data class Transaction(val state: TransactionState) : TxHistoryItemUM
|
||||
}
|
||||
}
|
||||
1
features/txhistory/impl/.gitignore
vendored
Normal file
1
features/txhistory/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
59
features/txhistory/impl/build.gradle.kts
Normal file
59
features/txhistory/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.txhistory.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/* Project - API */
|
||||
implementation(projects.features.txhistory.api)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.pagination)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
|
||||
/* AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.lifecycle.compose)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.shimmer)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/* Other */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.txhistory
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTxHistoryFeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : TxHistoryFeatureToggles {
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("TX_HISTORY_REFACTORING_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.txhistory.component
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.*
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.features.txhistory.model.TxHistoryModel
|
||||
import com.tangem.features.txhistory.ui.txHistoryItems
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal class DefaultTxHistoryComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: TxHistoryComponent.Params,
|
||||
) : TxHistoryComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: TxHistoryModel = getOrCreateModel(params)
|
||||
|
||||
override val txHistoryState: StateFlow<TxHistoryUM>
|
||||
get() = model.uiState
|
||||
|
||||
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) {
|
||||
txHistoryItems(listState, state)
|
||||
}
|
||||
|
||||
override fun reload() {
|
||||
model.reload()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : TxHistoryComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: TxHistoryComponent.Params): DefaultTxHistoryComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
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.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.txhistory.impl.R
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.toBriefAddressFormat
|
||||
|
||||
internal class TxHistoryItemToTransactionStateConverter(
|
||||
private val currency: CryptoCurrency,
|
||||
private val txHistoryUiActions: TxHistoryUiActions,
|
||||
) : Converter<TxHistoryItem, TransactionState> {
|
||||
override fun convert(value: TxHistoryItem): TransactionState {
|
||||
return TransactionState.Content(
|
||||
txHash = value.txHash,
|
||||
amount = value.getAmount(),
|
||||
time = value.timestampInMillis.toTimeFormat(),
|
||||
status = value.status.tiUiStatus(),
|
||||
direction = value.extractDirection(),
|
||||
iconRes = value.extractIcon(),
|
||||
title = value.extractTitle(),
|
||||
subtitle = value.extractSubtitle(),
|
||||
timestamp = value.timestampInMillis,
|
||||
onClick = { txHistoryUiActions.openTxInExplorer(value.txHash) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractIcon(): Int = if (status == TxHistoryItem.TransactionStatus.Failed) {
|
||||
R.drawable.ic_close_24
|
||||
} else {
|
||||
when (type) {
|
||||
is TxHistoryItem.TransactionType.Approve -> R.drawable.ic_doc_24
|
||||
is TxHistoryItem.TransactionType.Staking.Stake,
|
||||
is TxHistoryItem.TransactionType.Staking.Vote,
|
||||
is TxHistoryItem.TransactionType.Staking.Restake,
|
||||
-> R.drawable.ic_transaction_history_staking_24
|
||||
is TxHistoryItem.TransactionType.Staking.ClaimRewards,
|
||||
-> R.drawable.ic_transaction_history_claim_rewards_24
|
||||
is TxHistoryItem.TransactionType.Staking.Unstake,
|
||||
is TxHistoryItem.TransactionType.Staking.Withdraw,
|
||||
-> R.drawable.ic_transaction_history_unstaking_24
|
||||
is TxHistoryItem.TransactionType.Operation,
|
||||
is TxHistoryItem.TransactionType.Swap,
|
||||
is TxHistoryItem.TransactionType.Transfer,
|
||||
is TxHistoryItem.TransactionType.UnknownOperation,
|
||||
-> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractTitle(): TextReference = when (val type = type) {
|
||||
is TxHistoryItem.TransactionType.Approve -> resourceReference(R.string.common_approval)
|
||||
is TxHistoryItem.TransactionType.Operation -> stringReference(type.name)
|
||||
is TxHistoryItem.TransactionType.Swap -> resourceReference(R.string.common_swap)
|
||||
is TxHistoryItem.TransactionType.Transfer -> resourceReference(R.string.common_transfer)
|
||||
is TxHistoryItem.TransactionType.Staking.Stake -> resourceReference(R.string.common_stake)
|
||||
is TxHistoryItem.TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake)
|
||||
is TxHistoryItem.TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote)
|
||||
is TxHistoryItem.TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards)
|
||||
is TxHistoryItem.TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw)
|
||||
is TxHistoryItem.TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake)
|
||||
is TxHistoryItem.TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractSubtitle(): TextReference =
|
||||
when (val interactionAddress = interactionAddressType) {
|
||||
is TxHistoryItem.InteractionAddressType.Contract -> resourceReference(
|
||||
id = R.string.transaction_history_contract_address,
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
is TxHistoryItem.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 TxHistoryItem.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 TxHistoryItem.InteractionAddressType.Validator -> resourceReference(
|
||||
id = R.string.transaction_history_transaction_validator,
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
null -> {
|
||||
TextReference.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractDirection() =
|
||||
if (isOutgoing) TransactionState.Content.Direction.OUTGOING else TransactionState.Content.Direction.INCOMING
|
||||
|
||||
private fun TxHistoryItem.getAmount(): String {
|
||||
if (type is TxHistoryItem.TransactionType.Staking.Vote ||
|
||||
type == TxHistoryItem.TransactionType.Staking.ClaimRewards ||
|
||||
type == TxHistoryItem.TransactionType.Staking.Withdraw
|
||||
) {
|
||||
return ""
|
||||
}
|
||||
val prefix = when {
|
||||
status == TxHistoryItem.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 TxHistoryItem.TransactionStatus.tiUiStatus() = when (this) {
|
||||
TxHistoryItem.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed
|
||||
TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed
|
||||
TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.txhistory.di
|
||||
|
||||
import com.tangem.features.txhistory.DefaultTxHistoryFeatureToggles
|
||||
import com.tangem.features.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.features.txhistory.component.DefaultTxHistoryComponent
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface TxHistoryFeatureModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideFeatureToggles(featureToggles: DefaultTxHistoryFeatureToggles): TxHistoryFeatureToggles
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindComponentFactory(factory: DefaultTxHistoryComponent.Factory): TxHistoryComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.txhistory.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.txhistory.model.TxHistoryModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface TxHistoryModelModule {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TxHistoryModel::class)
|
||||
fun bindModel(model: TxHistoryModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.txhistory.di
|
||||
|
||||
import com.tangem.features.txhistory.entity.DefaultTxHistoryUpdater
|
||||
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUpdateListener
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object TxHistoryUpdaterModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTxHistoryContentContentUpdateEmitter(impl: DefaultTxHistoryUpdater): TxHistoryContentUpdateEmitter = impl
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTxHistoryUpdaterListener(impl: DefaultTxHistoryUpdater): TxHistoryUpdateListener = impl
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.txhistory.entity
|
||||
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class DefaultTxHistoryUpdater @Inject constructor() : TxHistoryUpdateListener, TxHistoryContentUpdateEmitter {
|
||||
|
||||
private val updateChannel = Channel<Unit>(Channel.BUFFERED)
|
||||
override val updates: Flow<Unit> = updateChannel.receiveAsFlow()
|
||||
|
||||
override suspend fun triggerUpdate() {
|
||||
updateChannel.send(Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.txhistory.entity
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal interface TxHistoryUpdateListener {
|
||||
val updates: Flow<Unit>
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
package com.tangem.features.txhistory.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
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.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUpdateListener
|
||||
import com.tangem.features.txhistory.utils.TxHistoryListManager
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TxHistoryModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val txHistoryUpdateListener: TxHistoryUpdateListener,
|
||||
repository: TxHistoryRepositoryV2,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model(), TxHistoryUiActions {
|
||||
|
||||
private val params: TxHistoryComponent.Params = paramsContainer.require()
|
||||
private val txHistoryItemConverter =
|
||||
TxHistoryItemToTransactionStateConverter(currency = params.currency, txHistoryUiActions = this)
|
||||
private val txHistoryListManager = TxHistoryListManager(
|
||||
repository = repository,
|
||||
dispatchers = dispatchers,
|
||||
userWalletId = params.userWalletId,
|
||||
currency = params.currency,
|
||||
txHistoryItemConverter = txHistoryItemConverter,
|
||||
txHistoryUiActions = this,
|
||||
)
|
||||
private val _uiState: MutableStateFlow<TxHistoryUM> =
|
||||
MutableStateFlow(TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = ::openExplorer))
|
||||
val uiState: StateFlow<TxHistoryUM> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
handleBalanceHiding()
|
||||
subscribeToUiItemChanges()
|
||||
loadTxInfo()
|
||||
subscribeToUpdateListener()
|
||||
subscribeOnCurrencyStatusUpdates()
|
||||
}
|
||||
|
||||
private fun subscribeToUiItemChanges() {
|
||||
txHistoryListManager.uiItems
|
||||
.onEach { updateState(it) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToUpdateListener() {
|
||||
txHistoryUpdateListener.updates
|
||||
.onEach { reload() }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun loadTxInfo() {
|
||||
_uiState.update { state -> getLoadingState(state.isBalanceHidden) }
|
||||
modelScope.launch {
|
||||
txHistoryItemsCountUseCase.invoke(userWalletId = params.userWalletId, currency = params.currency)
|
||||
.onLeft(::handleErrorState)
|
||||
.onRight { txHistoryListManager.startLoading() }
|
||||
}
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
// fast exit
|
||||
if (uiState.value is TxHistoryUM.NotSupported) return
|
||||
|
||||
_uiState.update { state ->
|
||||
if (state !is TxHistoryUM.Content) getLoadingState(state.isBalanceHidden) else state
|
||||
}
|
||||
modelScope.launch {
|
||||
txHistoryItemsCountUseCase.invoke(userWalletId = params.userWalletId, currency = params.currency)
|
||||
.onLeft(::handleErrorState)
|
||||
.onRight { txHistoryListManager.reload() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBalanceHiding() {
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.onEach { _uiState.update { state -> state.copySealed(isBalanceHidden = it.isBalanceHidden) } }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun loadMoreItems(): Boolean {
|
||||
modelScope.launch { txHistoryListManager.loadMore(params.userWalletId, params.currency) }
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateState(items: ImmutableList<TxHistoryUM.TxHistoryItemUM>) {
|
||||
_uiState.update { state ->
|
||||
if (state is TxHistoryUM.Content) {
|
||||
state.copy(items = items)
|
||||
} else {
|
||||
TxHistoryUM.Content(
|
||||
items = items,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
loadMore = ::loadMoreItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleErrorState(error: TxHistoryStateError) {
|
||||
_uiState.update { state ->
|
||||
when (error) {
|
||||
is TxHistoryStateError.DataError -> TxHistoryUM.Error(
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
onReloadClick = ::reload,
|
||||
onExploreClick = ::openExplorer,
|
||||
)
|
||||
TxHistoryStateError.EmptyTxHistories -> TxHistoryUM.Empty(
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
onExploreClick = ::openExplorer,
|
||||
)
|
||||
TxHistoryStateError.TxHistoryNotImplemented -> TxHistoryUM.NotSupported(
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
pendingTransactions = persistentListOf(),
|
||||
onExploreClick = ::openExplorer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLoadingState(isBalanceHidden: Boolean): TxHistoryUM.Loading {
|
||||
return TxHistoryUM.Loading(isBalanceHidden = isBalanceHidden, onExploreClick = ::openExplorer)
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyStatusUpdates() {
|
||||
val userWallet: UserWallet = requireNotNull(getUserWalletUseCase(params.userWalletId).getOrNull()) {
|
||||
"User wallet not found"
|
||||
}
|
||||
getCurrencyStatusUpdatesUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyId = params.currency.id,
|
||||
isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.onEach(::handlePendingTxsChanges)
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun handlePendingTxsChanges(maybeCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>) {
|
||||
maybeCurrencyStatus.onRight { status ->
|
||||
val pendingTxs = status.value.pendingTransactions
|
||||
.map(txHistoryItemConverter::convert)
|
||||
.toPersistentList()
|
||||
_uiState.update { state ->
|
||||
if (state is TxHistoryUM.NotSupported) {
|
||||
state.copy(pendingTransactions = pendingTxs)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun openExplorer() {
|
||||
params.openExplorer()
|
||||
}
|
||||
|
||||
override fun openTxInExplorer(txHash: String) {
|
||||
getExplorerTransactionUrlUseCase(
|
||||
txHash = txHash,
|
||||
networkId = params.currency.network.id,
|
||||
).fold(
|
||||
ifLeft = { Timber.e(it.toString()) },
|
||||
ifRight = { urlOpener.openUrl(url = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
package com.tangem.features.txhistory.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.components.transactions.PendingTxsBlock
|
||||
import com.tangem.core.ui.components.transactions.Transaction
|
||||
import com.tangem.core.ui.components.transactions.TxHistoryTitle
|
||||
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.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
|
||||
private const val LOAD_ITEMS_BUFFER = 20
|
||||
|
||||
internal fun LazyListScope.txHistoryItems(listState: LazyListState, state: TxHistoryUM) {
|
||||
when (state) {
|
||||
is TxHistoryUM.Content -> contentItems(listState, state)
|
||||
is TxHistoryUM.Empty -> nonContentItem(state = EmptyTransactionsBlockState.Empty(state.onExploreClick))
|
||||
is TxHistoryUM.Error -> nonContentItem(
|
||||
state = EmptyTransactionsBlockState.FailedToLoad(
|
||||
onReload = state.onReloadClick,
|
||||
onExplore = state.onExploreClick,
|
||||
),
|
||||
)
|
||||
is TxHistoryUM.Loading -> loadingItems(state)
|
||||
is TxHistoryUM.NotSupported -> {
|
||||
if (state.pendingTransactions.isNotEmpty()) {
|
||||
item(key = "PendingTxsBlock", contentType = "PendingTxsBlock") {
|
||||
PendingTxsBlock(pendingTxs = state.pendingTransactions, isBalanceHidden = state.isBalanceHidden)
|
||||
}
|
||||
}
|
||||
|
||||
nonContentItem(
|
||||
state = EmptyTransactionsBlockState.NotImplemented(onExplore = state.onExploreClick),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.nonContentItem(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) {
|
||||
item(key = state::class.java, contentType = state::class.java) {
|
||||
EmptyTransactionBlock(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.loadingItems(state: TxHistoryUM.Loading) {
|
||||
itemsIndexed(
|
||||
items = state.items,
|
||||
key = { _, item ->
|
||||
when (item) {
|
||||
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> item.itemKey
|
||||
is TxHistoryUM.TxHistoryItemUM.Title -> item.onExploreClick.hashCode()
|
||||
is TxHistoryUM.TxHistoryItemUM.Transaction ->
|
||||
item.state.txHash + (item.state as? TransactionState.Content)?.hashCode()
|
||||
}
|
||||
},
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { index, item ->
|
||||
TxHistoryListItem(
|
||||
state = item,
|
||||
isBalanceHidden = true,
|
||||
modifier = Modifier.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.items.lastIndex,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun LazyListScope.contentItems(listState: LazyListState, state: TxHistoryUM.Content) {
|
||||
itemsIndexed(
|
||||
items = state.items,
|
||||
key = { _, item ->
|
||||
when (item) {
|
||||
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> item.itemKey
|
||||
is TxHistoryUM.TxHistoryItemUM.Title -> item.onExploreClick.hashCode()
|
||||
is TxHistoryUM.TxHistoryItemUM.Transaction ->
|
||||
item.state.txHash + (item.state as? TransactionState.Content)?.hashCode()
|
||||
}
|
||||
},
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { index, item ->
|
||||
TxHistoryListItem(
|
||||
state = item,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = Modifier.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.items.lastIndex,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
item {
|
||||
InfiniteListHandler(
|
||||
listState = listState,
|
||||
buffer = LOAD_ITEMS_BUFFER,
|
||||
onLoadMore = state.loadMore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TxHistoryListItem(
|
||||
state: TxHistoryUM.TxHistoryItemUM,
|
||||
isBalanceHidden: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (state) {
|
||||
is TxHistoryUM.TxHistoryItemUM.GroupTitle -> {
|
||||
TxHistoryGroupTitle(config = state, modifier = modifier)
|
||||
}
|
||||
is TxHistoryUM.TxHistoryItemUM.Title -> {
|
||||
TxHistoryTitle(onExploreClick = state.onExploreClick, modifier = modifier)
|
||||
}
|
||||
is TxHistoryUM.TxHistoryItemUM.Transaction -> {
|
||||
Transaction(
|
||||
state = state.state,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TxHistoryGroupTitle(config: TxHistoryUM.TxHistoryItemUM.GroupTitle, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size24),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Text(
|
||||
text = config.title,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListBatchingContext
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListConfig
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchListState
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
private typealias TxHistoryBatchAction = BatchAction<Int, TxHistoryListConfig, Nothing>
|
||||
|
||||
internal class TxHistoryListManager(
|
||||
private val repository: TxHistoryRepositoryV2,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val userWalletId: UserWalletId,
|
||||
private val currency: CryptoCurrency,
|
||||
txHistoryItemConverter: TxHistoryItemToTransactionStateConverter,
|
||||
txHistoryUiActions: TxHistoryUiActions,
|
||||
) {
|
||||
|
||||
private val jobHolder = JobHolder()
|
||||
private val actionsFlow: MutableSharedFlow<TxHistoryBatchAction> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
private val state: MutableStateFlow<TxHistoryListState> = MutableStateFlow(TxHistoryListState())
|
||||
private val uiManager = TxHistoryUiManager(
|
||||
state = state,
|
||||
txHistoryItemConverter = txHistoryItemConverter,
|
||||
txHistoryUiActions = txHistoryUiActions,
|
||||
)
|
||||
|
||||
val uiItems: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = uiManager.items
|
||||
|
||||
suspend fun startLoading() = coroutineScope {
|
||||
val batchFlow = repository.getTxHistoryBatchFlow(
|
||||
context = TxHistoryListBatchingContext(
|
||||
actionsFlow = actionsFlow,
|
||||
coroutineScope = this,
|
||||
),
|
||||
batchSize = 50,
|
||||
)
|
||||
|
||||
batchFlow.state
|
||||
.onEach { state -> updateState(state) }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(scope = this)
|
||||
.saveIn(jobHolder)
|
||||
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TxHistoryListConfig(userWalletId, currency, refresh = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun reload() {
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TxHistoryListConfig(userWalletId, currency, refresh = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadMore(userWalletId: UserWalletId, currency: CryptoCurrency) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.LoadMore(
|
||||
requestParams = TxHistoryListConfig(userWalletId, currency, refresh = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateState(batchListState: BatchListState<Int, PaginationWrapper<TxHistoryItem>>) {
|
||||
state.update { state ->
|
||||
val clearUiBatches =
|
||||
state.status is PaginationStatus.InitialLoading && batchListState.status is PaginationStatus.Paginating
|
||||
state.copy(
|
||||
status = batchListState.status,
|
||||
uiBatches = uiManager.createOrUpdateUiBatches(
|
||||
newCurrencyBatches = batchListState.data,
|
||||
clearUiBatches = clearUiBatches,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
|
||||
data class TxHistoryListState(
|
||||
val status: PaginationStatus<*> = PaginationStatus.None,
|
||||
val uiBatches: List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> = listOf(),
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
internal interface TxHistoryUiActions {
|
||||
|
||||
fun openExplorer()
|
||||
fun openTxInExplorer(txHash: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.pagination.Batch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import java.util.UUID
|
||||
|
||||
internal class TxHistoryUiManager(
|
||||
private val state: MutableStateFlow<TxHistoryListState>,
|
||||
private val txHistoryItemConverter: TxHistoryItemToTransactionStateConverter,
|
||||
private val txHistoryUiActions: TxHistoryUiActions,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val items: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = state
|
||||
.mapLatest { state ->
|
||||
state.uiBatches.asSequence()
|
||||
.flatMap { it.data }
|
||||
.toImmutableList()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
fun createOrUpdateUiBatches(
|
||||
newCurrencyBatches: List<Batch<Int, PaginationWrapper<TxHistoryItem>>>,
|
||||
clearUiBatches: Boolean,
|
||||
): List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> {
|
||||
val currentUiBatches = state.value.uiBatches
|
||||
val batches = if (clearUiBatches) mutableListOf() else currentUiBatches.toMutableList()
|
||||
|
||||
for ((key, data) in newCurrencyBatches) {
|
||||
// Find if batch with same key exists
|
||||
val existingBatchIndex = batches.indexOfFirst { it.key == key }
|
||||
val shouldUpdateExisting = existingBatchIndex != -1 &&
|
||||
currentUiBatches[existingBatchIndex].data.transactionItemsSizeNotEqual(data.items)
|
||||
|
||||
// Case 1: Update existing batch if sizes differ
|
||||
if (shouldUpdateExisting) {
|
||||
val items = generateUiItems(key, data)
|
||||
batches[existingBatchIndex] = Batch(key = key, data = items)
|
||||
continue
|
||||
}
|
||||
|
||||
// Case 2: Skip if batch exists and has same size
|
||||
if (existingBatchIndex != -1) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Case 3: Create new batch
|
||||
val items = generateUiItems(key, data)
|
||||
batches.add(Batch(key = key, data = items))
|
||||
}
|
||||
|
||||
return batches
|
||||
}
|
||||
|
||||
private fun generateUiItems(key: Int, data: PaginationWrapper<TxHistoryItem>): List<TxHistoryUM.TxHistoryItemUM> {
|
||||
val items = mutableListOf<TxHistoryUM.TxHistoryItemUM>()
|
||||
|
||||
// Add title for the first batch
|
||||
if (key == 0) {
|
||||
items.add(TxHistoryUM.TxHistoryItemUM.Title(onExploreClick = txHistoryUiActions::openExplorer))
|
||||
}
|
||||
|
||||
// Process batch items only if there are any
|
||||
if (data.items.isNotEmpty()) {
|
||||
// Add first item with its group title
|
||||
val firstItem = data.items.first()
|
||||
val firstDate = firstItem.timestampInMillis.toDateFormatWithTodayYesterday()
|
||||
|
||||
items.add(
|
||||
TxHistoryUM.TxHistoryItemUM.GroupTitle(
|
||||
title = firstDate,
|
||||
itemKey = UUID.randomUUID().toString(),
|
||||
),
|
||||
)
|
||||
items.add(TxHistoryUM.TxHistoryItemUM.Transaction(txHistoryItemConverter.convert(firstItem)))
|
||||
|
||||
// Process remaining items with date separators when needed
|
||||
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 = UUID.randomUUID().toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
items.add(TxHistoryUM.TxHistoryItemUM.Transaction(txHistoryItemConverter.convert(next)))
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
private fun List<TxHistoryUM.TxHistoryItemUM>.transactionItemsSizeNotEqual(
|
||||
txHistoryItems: List<TxHistoryItem>,
|
||||
): Boolean {
|
||||
return this.filterIsInstance<TxHistoryUM.TxHistoryItemUM.Transaction>().size != txHistoryItems.size
|
||||
}
|
||||
}
|
||||
|
|
@ -224,6 +224,9 @@ include(":features:onramp:impl")
|
|||
|
||||
include(":features:stories:api")
|
||||
include(":features:stories:impl")
|
||||
|
||||
include(":features:txhistory:api")
|
||||
include(":features:txhistory:impl")
|
||||
// endregion Feature modules
|
||||
|
||||
// region Domain modules
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue