Updated on 2026-08-14
This commit is contained in:
parent
b98b66ff9f
commit
5b25eb6312
20 changed files with 635 additions and 48 deletions
|
|
@ -3,6 +3,8 @@ package com.tangem.datasource.di
|
|||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.txhistory.DefaultTxHistoryItemsStore
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.datasource.local.visa.DefaultTangemPayTxHistoryItemsStore
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -20,4 +22,12 @@ internal object TxHistoryItemsStoreModule {
|
|||
dataStore = RuntimeDataStore(),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayTxHistoryItemsStore(): TangemPayTxHistoryItemsStore {
|
||||
return DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore = RuntimeDataStore(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
|
||||
internal class DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore: StringKeyDataStore<Map<Int, List<VisaTxHistoryItem>>>,
|
||||
) : TangemPayTxHistoryItemsStore,
|
||||
StringKeyDataStoreDecorator<UserWalletId, Map<Int, List<VisaTxHistoryItem>>>(dataStore) {
|
||||
override fun provideStringKey(key: UserWalletId): String = key.stringValue
|
||||
|
||||
override suspend fun getSyncOrNull(key: UserWalletId, offset: Int): List<VisaTxHistoryItem>? {
|
||||
val storedValue = getSyncOrNull(key)
|
||||
return storedValue?.get(offset)
|
||||
}
|
||||
|
||||
override suspend fun store(key: UserWalletId, offset: Int, value: List<VisaTxHistoryItem>) {
|
||||
val oldValue = getSyncOrNull(key).orEmpty()
|
||||
val newValue = oldValue.toMutableMap().apply { put(offset, value) }
|
||||
store(key, newValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
|
||||
interface TangemPayTxHistoryItemsStore {
|
||||
|
||||
suspend fun getSyncOrNull(key: UserWalletId, offset: Int): List<VisaTxHistoryItem>?
|
||||
|
||||
suspend fun remove(key: UserWalletId)
|
||||
|
||||
suspend fun store(key: UserWalletId, offset: Int, value: List<VisaTxHistoryItem>)
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.repository.DefaultKycRepository
|
||||
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
|
||||
import com.tangem.data.pay.repository.DefaultOnboardingRepository
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -21,4 +23,8 @@ internal interface TangemPayDataModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindOnboardingRepository(repository: DefaultOnboardingRepository): OnboardingRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayTxHistoryRepository(repository: DefaultTangemPayTxHistoryRepository): TangemPayTxHistoryRepository
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.visa.utils.VisaApiRequestMaker
|
||||
import com.tangem.data.visa.utils.VisaTxHistoryItemConverter
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.BatchListSource
|
||||
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
|
||||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("UnusedPrivateMember", "MagicNumber", "UnnecessaryParentheses")
|
||||
internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
||||
private val visaApiRequestMaker: VisaApiRequestMaker,
|
||||
private val visaApi: TangemPayApi,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : TangemPayTxHistoryRepository {
|
||||
|
||||
override fun getTxHistoryBatchFlow(
|
||||
batchSize: Int,
|
||||
context: TangemPayTxHistoryListBatchingContext,
|
||||
): TangemPayTxHistoryListBatchFlow {
|
||||
return BatchListSource(
|
||||
fetchDispatcher = dispatchers.io,
|
||||
context = context,
|
||||
generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: 0 },
|
||||
batchFetcher = createFetcher(batchSize),
|
||||
).toBatchFlow()
|
||||
}
|
||||
|
||||
private fun createFetcher(
|
||||
batchSize: Int,
|
||||
): LimitOffsetBatchFetcher<TangemPayTxHistoryListConfig, List<VisaTxHistoryItem>> {
|
||||
return LimitOffsetBatchFetcher(
|
||||
prefetchDistance = batchSize,
|
||||
batchSize = batchSize,
|
||||
subFetcher = { config, _, isInitialLoading ->
|
||||
val items = loadItems(config = config.params, offset = config.offset, limit = config.limit)
|
||||
BatchFetchResult.Success(
|
||||
data = items,
|
||||
last = items.size < batchSize,
|
||||
empty = items.isEmpty(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadItems(
|
||||
config: TangemPayTxHistoryListConfig,
|
||||
offset: Int,
|
||||
limit: Int,
|
||||
): List<VisaTxHistoryItem> {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getCacheKey(userWalletId = config.userWalletId, offset = offset),
|
||||
skipCache = config.refresh,
|
||||
// TODO: TangemPay uncomment while BFF will be ready
|
||||
// block = { fetch(userWalletId = config.userWalletId, offset = offset, pageSize = limit) },
|
||||
block = { fetchMocked(userWalletId = config.userWalletId) },
|
||||
)
|
||||
|
||||
return txHistoryItemsStore.getSyncOrNull(key = config.userWalletId, offset = offset).orEmpty()
|
||||
}
|
||||
|
||||
private fun getCacheKey(userWalletId: UserWalletId, offset: Int): String {
|
||||
return "tangem_pay_tx_history_${userWalletId}_$offset"
|
||||
}
|
||||
|
||||
private suspend fun fetch(userWalletId: UserWalletId, offset: Int, pageSize: Int) {
|
||||
val response = visaApiRequestMaker.request(userWalletId = userWalletId) { authHeader, accessCodeData ->
|
||||
visaApi.getTxHistory(
|
||||
authHeader = authHeader,
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
limit = pageSize,
|
||||
offset = offset,
|
||||
)
|
||||
}
|
||||
val items = VisaTxHistoryItemConverter.convertList(response.transactions)
|
||||
|
||||
txHistoryItemsStore.store(key = userWalletId, offset = offset, value = items)
|
||||
}
|
||||
|
||||
private suspend fun fetchMocked(userWalletId: UserWalletId) {
|
||||
delay(2000) // initiating network request
|
||||
val items = VisaTxHistoryItemConverter.convertList(MOCKED_RESPONSE.transactions)
|
||||
txHistoryItemsStore.store(key = userWalletId, offset = 0, value = items)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val now = DateTime.now()
|
||||
private var transactionIdCounter = 1000L
|
||||
|
||||
private val todayTransactions = List(2) { index ->
|
||||
VisaTxHistoryResponse.Transaction(
|
||||
transactionId = transactionIdCounter++,
|
||||
transactionDt = now.minusHours(index + 1).minusMinutes(index * 15),
|
||||
transactionStatus = "Completed", // As "Success" might map to "Completed"
|
||||
transactionType = "Purchase",
|
||||
billingAmount = BigDecimal("${10 + index * 5}.${20 + index * 3}"),
|
||||
billingCurrencyCode = 840, // USD
|
||||
transactionAmount = BigDecimal("${10 + index * 5}.${20 + index * 3}"),
|
||||
transactionCurrencyCode = 840, // USD
|
||||
merchantName = "Online Store ${'A' + index}",
|
||||
merchantCity = "San Francisco",
|
||||
merchantCountryCode = "US",
|
||||
merchantCategoryCode = "5411", // Grocery Stores
|
||||
authCode = "AUTH${12345 + index}",
|
||||
rrn = "RRN00${100 + index}",
|
||||
blockchainAmount = BigDecimal("0.001").multiply(BigDecimal(index + 1)),
|
||||
blockchainCoinName = "ETH",
|
||||
blockchainFee = BigDecimal("0.00005"),
|
||||
requests = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private val yesterdayTransactions = List(4) { index ->
|
||||
VisaTxHistoryResponse.Transaction(
|
||||
transactionId = transactionIdCounter++,
|
||||
transactionDt = now.minusDays(1).withTime(10 + index * 2, 15 * index % 60, index * 5 % 60, 0),
|
||||
transactionStatus = "Completed",
|
||||
transactionType = "Purchase",
|
||||
billingAmount = BigDecimal("${20 + index * 7}.${10 + index * 2}"),
|
||||
billingCurrencyCode = 840, // USD
|
||||
transactionAmount = BigDecimal("${20 + index * 7}.${10 + index * 2}"),
|
||||
transactionCurrencyCode = 840, // USD
|
||||
merchantName = "Coffee Shop ${'X' + index}",
|
||||
merchantCity = "Berlin",
|
||||
merchantCountryCode = "DE",
|
||||
merchantCategoryCode = "5812", // Restaurants
|
||||
authCode = "AUTH${22345 + index}",
|
||||
rrn = "RRN00${200 + index}",
|
||||
blockchainAmount = BigDecimal("0.001").multiply(BigDecimal(index + 1)),
|
||||
blockchainCoinName = "ETH",
|
||||
blockchainFee = BigDecimal("0.00005"),
|
||||
requests = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private val threeDaysAgoTransactions = List(10) { index ->
|
||||
VisaTxHistoryResponse.Transaction(
|
||||
transactionId = transactionIdCounter++,
|
||||
transactionDt = now.minusDays(3).withTime(9 + index, (index * 10) % 60, (index * 20) % 60, 0),
|
||||
transactionStatus = "Completed",
|
||||
transactionType = "Purchase",
|
||||
billingAmount = BigDecimal("${5 + index * 2}.${50 + index}"),
|
||||
billingCurrencyCode = 840, // USD
|
||||
transactionAmount = BigDecimal("${5 + index * 2}.${50 + index}"),
|
||||
transactionCurrencyCode = 840, // USD
|
||||
merchantName = "Gadget Store ${'M' + index}",
|
||||
merchantCity = if (index % 2 == 0) "London" else "Paris",
|
||||
merchantCountryCode = if (index % 2 == 0) "GB" else "FR",
|
||||
merchantCategoryCode = "5732", // Electronic Sales
|
||||
authCode = "AUTH${32345 + index}",
|
||||
rrn = "RRN00${300 + index}",
|
||||
blockchainAmount = BigDecimal("0.001").multiply(BigDecimal(index + 1)),
|
||||
blockchainCoinName = "ETH",
|
||||
blockchainFee = BigDecimal("0.00005"),
|
||||
requests = emptyList(),
|
||||
)
|
||||
}
|
||||
private val MOCKED_RESPONSE = VisaTxHistoryResponse(
|
||||
cardWalletAddress = "0xYourVisaVirtualCardAddressHere",
|
||||
transactions = todayTransactions + yesterdayTransactions + threeDaysAgoTransactions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object VisaTxHistoryItemConverter : Converter<VisaTxHistoryResponse.Transaction, VisaTxHistoryItem> {
|
||||
|
||||
override fun convert(value: VisaTxHistoryResponse.Transaction): VisaTxHistoryItem {
|
||||
return VisaTxHistoryItem(
|
||||
id = value.transactionId.toString(),
|
||||
date = value.transactionDt,
|
||||
amount = value.blockchainAmount,
|
||||
fiatAmount = value.transactionAmount,
|
||||
merchantName = value.merchantName,
|
||||
status = value.transactionStatus,
|
||||
fiatCurrency = findCurrencyByNumericCode(value.transactionCurrencyCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
|
||||
internal class VisaTxHistoryItemFactory {
|
||||
|
||||
fun create(transaction: VisaTxHistoryResponse.Transaction): VisaTxHistoryItem {
|
||||
return VisaTxHistoryItem(
|
||||
id = transaction.transactionId.toString(),
|
||||
date = transaction.transactionDt,
|
||||
amount = transaction.blockchainAmount,
|
||||
fiatAmount = transaction.transactionAmount,
|
||||
merchantName = transaction.merchantName,
|
||||
status = transaction.transactionStatus,
|
||||
fiatCurrency = findCurrencyByNumericCode(transaction.transactionCurrencyCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,6 @@ internal class VisaTxHistoryPagingSource(
|
|||
val requestTxHistory: suspend (offset: Int, pageSize: Int) -> VisaTxHistoryResponse,
|
||||
) : PagingSource<Int, VisaTxHistoryItem>() {
|
||||
|
||||
private val itemsFactory = VisaTxHistoryItemFactory()
|
||||
|
||||
private val cardPublicKey = params.cardPublicKey
|
||||
private val pageSize = params.pageSize
|
||||
private val isRefresh = params.isRefresh
|
||||
|
|
@ -82,7 +80,7 @@ internal class VisaTxHistoryPagingSource(
|
|||
|
||||
pagedItems.update {
|
||||
it.toMutableMap().apply {
|
||||
this[offset] = response.transactions.map(itemsFactory::create)
|
||||
this[offset] = response.transactions.map(VisaTxHistoryItemConverter::convert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,17 +10,18 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
/** Domain models */
|
||||
api(projects.domain.visa.models)
|
||||
|
||||
/** Project - Domain */
|
||||
/** Project - Core */
|
||||
api(projects.core.pagination)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.error)
|
||||
|
||||
/** Project - Domain */
|
||||
api(projects.domain.models)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
api(projects.domain.visa.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Security */
|
||||
implementation(deps.spongecastle.core)
|
||||
|
|
|
|||
|
|
@ -12,4 +12,6 @@ data class VisaTxHistoryItem(
|
|||
val merchantName: String?,
|
||||
val status: String,
|
||||
val fiatCurrency: Currency,
|
||||
)
|
||||
) {
|
||||
val timeStampInMillis: Long = date.millis
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.tangempay.model
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
data class TangemPayTxHistoryListConfig(val userWalletId: UserWalletId, val refresh: Boolean)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.domain.tangempay.model
|
||||
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.pagination.BatchFlow
|
||||
import com.tangem.pagination.BatchingContext
|
||||
|
||||
typealias TangemPayTxHistoryListBatchingContext = BatchingContext<Int, TangemPayTxHistoryListConfig, Nothing>
|
||||
|
||||
typealias TangemPayTxHistoryListBatchFlow = BatchFlow<Int, List<VisaTxHistoryItem>, Nothing>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.tangempay.repository
|
||||
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
|
||||
|
||||
interface TangemPayTxHistoryRepository {
|
||||
fun getTxHistoryBatchFlow(
|
||||
batchSize: Int,
|
||||
context: TangemPayTxHistoryListBatchingContext,
|
||||
): TangemPayTxHistoryListBatchFlow
|
||||
}
|
||||
|
|
@ -25,6 +25,8 @@ dependencies {
|
|||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.visa)
|
||||
implementation(projects.domain.visa.models)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
|
|
|
|||
|
|
@ -5,16 +5,15 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
|
||||
import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent
|
||||
import com.tangem.features.tangempay.utils.TangemPayTxHistoryListManager
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -24,39 +23,106 @@ import javax.inject.Inject
|
|||
internal class TangemPayTxHistoryModel @Inject constructor(
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
tangemPayTxHistoryRepository: TangemPayTxHistoryRepository,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
) : Model(), TxHistoryUiActions {
|
||||
|
||||
private val params: DefaultTangemPayTxHistoryComponent.Params = paramsContainer.require()
|
||||
private val listManager = TangemPayTxHistoryListManager(
|
||||
repository = tangemPayTxHistoryRepository,
|
||||
dispatchers = dispatchers,
|
||||
userWalletId = params.userWalletId,
|
||||
txHistoryUiActions = this,
|
||||
)
|
||||
|
||||
val uiState: StateFlow<TxHistoryUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
field = MutableStateFlow<TxHistoryUM>(getLoadingState(isBalanceHidden = true))
|
||||
|
||||
init {
|
||||
handleBalanceHiding()
|
||||
launchPagination()
|
||||
subscribeToUiItemChanges()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun launchPagination() {
|
||||
modelScope.launch { listManager.launchPagination() }
|
||||
}
|
||||
|
||||
private fun subscribeToUiItemChanges() {
|
||||
modelScope.launch {
|
||||
Timber.d("subscribeToUiItemChanges: ${params.userWalletId}")
|
||||
delay(2000)
|
||||
uiState.update { PreviewTangemPayTxHistoryComponent.contentUM }
|
||||
listManager.uiItems
|
||||
.onEach { updateState(it) }
|
||||
.launchIn(modelScope)
|
||||
listManager.paginationStatus
|
||||
.onEach { paginationStatus -> handlePaginationStatus(paginationStatus) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
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 handlePaginationStatus(status: PaginationStatus<*>) {
|
||||
uiState.update { state ->
|
||||
when (status) {
|
||||
is PaginationStatus.InitialLoadingError -> getErrorState(state.isBalanceHidden)
|
||||
PaginationStatus.EndOfPagination,
|
||||
PaginationStatus.InitialLoading,
|
||||
PaginationStatus.NextBatchLoading,
|
||||
PaginationStatus.None,
|
||||
is PaginationStatus.Paginating<*>,
|
||||
-> state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMoreItems(): Boolean {
|
||||
modelScope.launch { listManager.loadMore(params.userWalletId) }
|
||||
return true
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
// fast exit
|
||||
if (uiState.value is TxHistoryUM.NotSupported) return
|
||||
|
||||
uiState.update { state ->
|
||||
state as? TxHistoryUM.Content ?: getLoadingState(state.isBalanceHidden)
|
||||
}
|
||||
modelScope.launch { listManager.reload() }
|
||||
}
|
||||
|
||||
private fun handleBalanceHiding() {
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.onEach { uiState.update { state -> state.copySealed(isBalanceHidden = it.isBalanceHidden) } }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onExploreClick() {
|
||||
override fun openExplorer() {
|
||||
Timber.d("onExploreClick: open explorer")
|
||||
}
|
||||
|
||||
private fun getInitialState(): TxHistoryUM {
|
||||
return TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = ::onExploreClick)
|
||||
override fun openTxInExplorer(txHash: String) {
|
||||
Timber.d("openTxInExplorer: $txHash")
|
||||
}
|
||||
|
||||
private fun getErrorState(isBalanceHidden: Boolean): TxHistoryUM.Error {
|
||||
return TxHistoryUM.Error(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onReloadClick = ::reload,
|
||||
onExploreClick = ::openExplorer,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLoadingState(isBalanceHidden: Boolean): TxHistoryUM.Loading {
|
||||
return TxHistoryUM.Loading(isBalanceHidden = isBalanceHidden, onExploreClick = ::openExplorer)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.tangempay.model.transformers
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.extensions.capitalize
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import org.joda.time.DateTimeZone
|
||||
|
||||
internal class TangemPayTxHistoryItemsConverter(
|
||||
private val txHistoryUiActions: TxHistoryUiActions,
|
||||
) : Converter<VisaTxHistoryItem, TransactionState> {
|
||||
override fun convert(value: VisaTxHistoryItem): TransactionState {
|
||||
val localDate = value.date.withZone(DateTimeZone.getDefault())
|
||||
val fiatAmount = value.fiatAmount.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = value.fiatCurrency.currencyCode,
|
||||
fiatCurrencySymbol = value.fiatCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
return TransactionState.Content(
|
||||
txHash = value.id,
|
||||
amount = "${StringsSigns.MINUS}$fiatAmount",
|
||||
time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter),
|
||||
status = TransactionState.Content.Status.Confirmed,
|
||||
direction = TransactionState.Content.Direction.OUTGOING,
|
||||
iconRes = R.drawable.ic_arrow_up_24,
|
||||
title = stringReference(value = value.merchantName?.capitalize() ?: "Unknown merchant"),
|
||||
subtitle = stringReference("How to get merchant type?"),
|
||||
timestamp = localDate.millis,
|
||||
onClick = { txHistoryUiActions.openTxInExplorer(value.id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.features.tangempay.utils
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
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 TangemPayTxHistoryBatchAction = BatchAction<Int, TangemPayTxHistoryListConfig, Nothing>
|
||||
|
||||
internal class TangemPayTxHistoryListManager(
|
||||
private val repository: TangemPayTxHistoryRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val userWalletId: UserWalletId,
|
||||
private val txHistoryUiActions: TxHistoryUiActions,
|
||||
) {
|
||||
private val jobHolder = JobHolder()
|
||||
private val actionsFlow: MutableSharedFlow<TangemPayTxHistoryBatchAction> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
private val state: MutableStateFlow<TangemPayTxHistoryState> = MutableStateFlow(TangemPayTxHistoryState())
|
||||
private val uiManager = TangemPayTxHistoryUiManager(state = state, txHistoryUiActions = txHistoryUiActions)
|
||||
|
||||
val uiItems: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = uiManager.items
|
||||
val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged()
|
||||
|
||||
suspend fun launchPagination() = coroutineScope {
|
||||
val batchFlow = repository.getTxHistoryBatchFlow(
|
||||
context = TangemPayTxHistoryListBatchingContext(actionsFlow = actionsFlow, coroutineScope = this),
|
||||
batchSize = 50,
|
||||
)
|
||||
|
||||
batchFlow.state
|
||||
.onEach { state -> updateState(state) }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(scope = this)
|
||||
.saveIn(jobHolder)
|
||||
|
||||
// Initial load
|
||||
reload()
|
||||
}
|
||||
|
||||
suspend fun reload() {
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TangemPayTxHistoryListConfig(userWalletId = userWalletId, refresh = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadMore(userWalletId: UserWalletId) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.LoadMore(
|
||||
requestParams = TangemPayTxHistoryListConfig(userWalletId, refresh = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateState(batchListState: BatchListState<Int, List<VisaTxHistoryItem>>) {
|
||||
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.tangempay.utils
|
||||
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
|
||||
data class TangemPayTxHistoryState(
|
||||
val status: PaginationStatus<*> = PaginationStatus.None,
|
||||
val uiBatches: List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> = listOf(),
|
||||
)
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.tangem.features.tangempay.utils
|
||||
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryItemsConverter
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
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.filter
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import java.util.UUID
|
||||
|
||||
internal class TangemPayTxHistoryUiManager(
|
||||
private val state: MutableStateFlow<TangemPayTxHistoryState>,
|
||||
private val txHistoryUiActions: TxHistoryUiActions,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val items: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = state
|
||||
// filter initial states, since we dont emit loading items as UI items
|
||||
.filter {
|
||||
it.status !is PaginationStatus.None &&
|
||||
it.status !is PaginationStatus.InitialLoading &&
|
||||
it.status !is PaginationStatus.InitialLoadingError
|
||||
}
|
||||
.mapLatest { state ->
|
||||
state.uiBatches.asSequence()
|
||||
.flatMap { it.data }
|
||||
.toImmutableList()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
private val txHistoryItemConverter = TangemPayTxHistoryItemsConverter(txHistoryUiActions = txHistoryUiActions)
|
||||
|
||||
fun createOrUpdateUiBatches(
|
||||
newCurrencyBatches: List<Batch<Int, List<VisaTxHistoryItem>>>,
|
||||
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)
|
||||
|
||||
// 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: List<VisaTxHistoryItem>): 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.isNotEmpty()) {
|
||||
// Add first item with its group title
|
||||
val firstItem = data.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.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(
|
||||
txInfos: List<VisaTxHistoryItem>,
|
||||
): Boolean {
|
||||
return this.filterIsInstance<TxHistoryUM.TxHistoryItemUM.Transaction>().size != txInfos.size
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
internal interface TxHistoryUiActions {
|
||||
interface TxHistoryUiActions {
|
||||
|
||||
fun openExplorer()
|
||||
fun openTxInExplorer(txHash: String)
|
||||
Loading…
Add table
Add a link
Reference in a new issue