Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-21 15:32:47 +04:00
parent 6d3a1d6e89
commit 99a8c6623b
15 changed files with 328 additions and 171 deletions

View file

@ -9,6 +9,8 @@ import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
private const val TX_HISTORY_PAGING_DEFAULT_LIMIT = 20
@Suppress("TooManyFunctions")
interface TangemPayApi {
@ -107,6 +109,13 @@ interface TangemPayApi {
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
@GET("v1/customer/transactions")
suspend fun getTangemPayTxHistory(
@Header("Authorization") authHeader: String,
@Query("cursor") cursor: String?,
@Query("limit") limit: Int = TX_HISTORY_PAGING_DEFAULT_LIMIT,
): ApiResponse<TangemPayTxHistoryResponse>
@GET("v1/customer/kyc")
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>

View file

@ -0,0 +1,83 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class TangemPayTxHistoryResponse(
@Json(name = "error") val error: String?,
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "transactions") val transactions: List<Transaction>,
)
@JsonClass(generateAdapter = true)
data class Transaction(
@Json(name = "id") val id: String, // UUID (used as cursor for pagination)
@Json(name = "type") val type: String, // "SPEND", "COLLATERAL", "PAYMENT", "FEE"
@Json(name = "spend") val spend: Spend? = null,
@Json(name = "collateral") val collateral: Collateral? = null,
@Json(name = "payment") val payment: Payment? = null,
@Json(name = "fee") val fee: Fee? = null,
)
@JsonClass(generateAdapter = true)
data class Spend(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "local_amount") val localAmount: BigDecimal? = null,
@Json(name = "local_currency") val localCurrency: String? = null,
@Json(name = "authorized_amount") val authorizedAmount: BigDecimal? = null,
@Json(name = "authorization_method") val authorizationMethod: String? = null,
@Json(name = "memo") val memo: String? = null,
@Json(name = "receipt") val receipt: Boolean? = null,
@Json(name = "merchant_name") val merchantName: String? = null,
@Json(name = "merchant_category") val merchantCategory: String? = null,
@Json(name = "merchant_category_code") val merchantCategoryCode: String? = null,
@Json(name = "merchant_id") val merchantId: String? = null,
@Json(name = "enriched_merchant_icon") val enrichedMerchantIcon: String? = null,
@Json(name = "enriched_merchant_name") val enrichedMerchantName: String? = null,
@Json(name = "enriched_merchant_category") val enrichedMerchantCategory: String? = null,
@Json(name = "card_id") val cardId: String? = null,
@Json(name = "card_type") val cardType: String? = null,
@Json(name = "status") val status: String? = null,
@Json(name = "declined_reason") val declinedReason: String? = null,
@Json(name = "authorized_at") val authorizedAt: DateTime? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
@JsonClass(generateAdapter = true)
data class Collateral(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "memo") val memo: String? = null,
@Json(name = "chain_id") val chainId: Long? = null,
@Json(name = "wallet_address") val walletAddress: String? = null,
@Json(name = "transaction_hash") val transactionHash: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
@JsonClass(generateAdapter = true)
data class Payment(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "memo") val memo: String? = null,
@Json(name = "chain_id") val chainId: Long? = null,
@Json(name = "wallet_address") val walletAddress: String? = null,
@Json(name = "transaction_hash") val transactionHash: String? = null,
@Json(name = "status") val status: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
@JsonClass(generateAdapter = true)
data class Fee(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "description") val description: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
}

View file

@ -3,22 +3,22 @@ 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
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
internal class DefaultTangemPayTxHistoryItemsStore(
dataStore: StringKeyDataStore<Map<Int, List<VisaTxHistoryItem>>>,
dataStore: StringKeyDataStore<Map<String, List<TangemPayTxHistoryItem>>>,
) : TangemPayTxHistoryItemsStore,
StringKeyDataStoreDecorator<UserWalletId, Map<Int, List<VisaTxHistoryItem>>>(dataStore) {
StringKeyDataStoreDecorator<UserWalletId, Map<String, List<TangemPayTxHistoryItem>>>(dataStore) {
override fun provideStringKey(key: UserWalletId): String = key.stringValue
override suspend fun getSyncOrNull(key: UserWalletId, offset: Int): List<VisaTxHistoryItem>? {
override suspend fun getSyncOrNull(key: UserWalletId, cursor: String): List<TangemPayTxHistoryItem>? {
val storedValue = getSyncOrNull(key)
return storedValue?.get(offset)
return storedValue?.get(cursor)
}
override suspend fun store(key: UserWalletId, offset: Int, value: List<VisaTxHistoryItem>) {
override suspend fun store(key: UserWalletId, cursor: String, value: List<TangemPayTxHistoryItem>) {
val oldValue = getSyncOrNull(key).orEmpty()
val newValue = oldValue.toMutableMap().apply { put(offset, value) }
val newValue = oldValue.toMutableMap().apply { put(cursor, value) }
store(key, newValue)
}
}

View file

@ -1,13 +1,13 @@
package com.tangem.datasource.local.visa
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
interface TangemPayTxHistoryItemsStore {
suspend fun getSyncOrNull(key: UserWalletId, offset: Int): List<VisaTxHistoryItem>?
suspend fun getSyncOrNull(key: UserWalletId, cursor: String): List<TangemPayTxHistoryItem>?
suspend fun remove(key: UserWalletId)
suspend fun store(key: UserWalletId, offset: Int, value: List<VisaTxHistoryItem>)
suspend fun store(key: UserWalletId, cursor: String, value: List<TangemPayTxHistoryItem>)
}

View file

@ -0,0 +1,92 @@
package com.tangem.pagination.fetcher
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.exception.EndOfPaginationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
/**
* First page: cursor = null
* Next pages: cursor = cursorFromItem(lastItemOfPreviousPage)
*/
class CursorBatchFetcher<TRequestParams : Any, TItem : Any>(
private val prefetchDistance: Int,
private val batchSize: Int,
private val subFetcher: SubFetcher<TRequestParams, TItem>,
private val cursorFromItem: (TItem) -> String,
) : BatchFetcher<TRequestParams, List<TItem>> {
data class Request<TRequestParams>(
val limit: Int,
val cursor: String?,
val params: TRequestParams,
)
fun interface SubFetcher<TRequestParams : Any, TItem : Any> {
suspend fun fetch(
request: Request<TRequestParams>,
lastResult: BatchFetchResult<List<TItem>>?,
isFirstBatchFetching: Boolean,
): BatchFetchResult<List<TItem>>
}
private val lastRequest = MutableStateFlow<Request<TRequestParams>?>(null)
override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult<List<TItem>> {
val request = Request(
cursor = null,
limit = prefetchDistance,
params = requestParams,
)
val result = runCatching {
subFetcher.fetch(request = request, lastResult = null, isFirstBatchFetching = true)
}.getOrElse {
currentCoroutineContext().ensureActive()
return BatchFetchResult.Error(it)
}
lastRequest.value = request
return result
}
override suspend fun fetchNext(
overrideRequestParams: TRequestParams?,
lastResult: BatchFetchResult<List<TItem>>,
): BatchFetchResult<List<TItem>> {
val lastRequest = requireNotNull(lastRequest.value) { "fetchFirst() must be called before fetchNext()" }
if (lastResult is BatchFetchResult.Success && lastResult.last && overrideRequestParams == null) {
return BatchFetchResult.Error(EndOfPaginationException())
}
val nextReq: Request<TRequestParams> =
if (lastResult is BatchFetchResult.Success<List<TItem>>) {
val items = lastResult.data
if (items.isEmpty()) {
return BatchFetchResult.Error(EndOfPaginationException())
}
val nextCursor = cursorFromItem(items.last())
Request(
cursor = nextCursor,
limit = batchSize,
params = overrideRequestParams ?: lastRequest.params,
)
} else {
lastRequest.copy(limit = batchSize, params = overrideRequestParams ?: lastRequest.params)
}
val result = runCatching {
subFetcher.fetch(request = nextReq, lastResult = lastResult, isFirstBatchFetching = false)
}.getOrElse {
currentCoroutineContext().ensureActive()
return BatchFetchResult.Error(it)
}
this.lastRequest.value = nextReq
return result
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.data.pay.repository
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.domain.pay.KycStartInfo
import com.tangem.domain.pay.repository.KycRepository
@ -16,9 +15,9 @@ internal class DefaultKycRepository @Inject constructor(
override suspend fun getKycStartInfo() = withContext(dispatchers.io) {
requestHelper.request { authHeader ->
tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result
tangemPayApi.getKycAccess(authHeader = authHeader)
}.map {
KycStartInfo(token = it.token, locale = it.locale)
KycStartInfo(token = it.result.token, locale = it.result.locale)
}
}
}

View file

@ -3,13 +3,11 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.ProductInstance
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError
import javax.inject.Inject
private const val VALID_STATUS = "valid"
@ -21,19 +19,17 @@ internal class DefaultOnboardingRepository @Inject constructor(
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> = either {
return requestHelper.request {
tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)).getOrThrow().result
?: raise(VisaApiError.UnknownWithoutCode)
}.map { result -> result.status == VALID_STATUS }
tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link))
}.map { it.result?.status == VALID_STATUS }
}
override suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo> = either {
return requestHelper.request { authHeader ->
val response = tangemPayApi.getCustomerMe(authHeader).getOrThrow()
response.result ?: raise(VisaApiError.UnknownWithoutCode)
}.map { result ->
tangemPayApi.getCustomerMe(authHeader)
}.map {
CustomerInfo(
productInstance = result.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
kycStatus = result.kyc?.status,
productInstance = it.result?.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
kycStatus = it.result?.kyc?.status,
)
}
}

View file

@ -1,30 +1,27 @@
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.data.visa.utils.TangemPayTxHistoryItemConverter
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.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.BatchListSource
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
import com.tangem.pagination.fetcher.BatchFetcher
import com.tangem.pagination.fetcher.CursorBatchFetcher
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")
private const val INITIAL_CURSOR = "initial_cursor_key"
internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
private val visaApiRequestMaker: VisaApiRequestMaker,
private val requestPerformer: TangemPayRequestPerformer,
private val visaApi: TangemPayApi,
private val cacheRegistry: CacheRegistry,
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
@ -45,137 +42,54 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
private fun createFetcher(
batchSize: Int,
): LimitOffsetBatchFetcher<TangemPayTxHistoryListConfig, List<VisaTxHistoryItem>> {
return LimitOffsetBatchFetcher(
): BatchFetcher<TangemPayTxHistoryListConfig, List<TangemPayTxHistoryItem>> {
return CursorBatchFetcher(
prefetchDistance = batchSize,
batchSize = batchSize,
subFetcher = { config, _, isInitialLoading ->
val items = loadItems(config = config.params, offset = config.offset, limit = config.limit)
subFetcher = { request, _, _ ->
val items = loadItems(config = request.params, cursor = request.cursor, limit = request.limit)
BatchFetchResult.Success(
data = items,
last = items.size < batchSize,
last = items.size < request.limit,
empty = items.isEmpty(),
)
},
cursorFromItem = { item -> item.id }, // last items id becomes next cursor
)
}
private suspend fun loadItems(
config: TangemPayTxHistoryListConfig,
offset: Int,
cursor: String?,
limit: Int,
): List<VisaTxHistoryItem> {
): List<TangemPayTxHistoryItem> {
cacheRegistry.invokeOnExpire(
key = getCacheKey(userWalletId = config.userWalletId, offset = offset),
key = getCacheKey(userWalletId = config.userWalletId, cursor = cursor),
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) },
block = { fetch(userWalletId = config.userWalletId, cursor = cursor, pageSize = limit) },
)
return txHistoryItemsStore.getSyncOrNull(key = config.userWalletId, offset = offset).orEmpty()
return txHistoryItemsStore.getSyncOrNull(
key = config.userWalletId,
cursor = cursor ?: INITIAL_CURSOR,
).orEmpty()
}
private fun getCacheKey(userWalletId: UserWalletId, offset: Int): String {
return "tangem_pay_tx_history_${userWalletId}_$offset"
private fun getCacheKey(userWalletId: UserWalletId, cursor: String?): String {
return "tangem_pay_tx_history_${userWalletId}_${cursor ?: INITIAL_CURSOR}"
}
private suspend fun fetch(userWalletId: UserWalletId, offset: Int, pageSize: Int) {
val response = visaApiRequestMaker.request(userWalletId = userWalletId) { authHeader, accessCodeData ->
visaApi.getTxHistory(
private suspend fun fetch(userWalletId: UserWalletId, cursor: String?, pageSize: Int) {
val response = requestPerformer.request { authHeader ->
visaApi.getTangemPayTxHistory(
authHeader = authHeader,
customerId = accessCodeData.customerId,
productInstanceId = accessCodeData.productInstanceId,
limit = pageSize,
offset = offset,
cursor = cursor,
)
}.getOrNull()
response?.let {
val items = TangemPayTxHistoryItemConverter.convertList(response.result.transactions)
txHistoryItemsStore.store(key = userWalletId, cursor = cursor ?: INITIAL_CURSOR, value = items)
}
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,
)
}
}

View file

@ -4,7 +4,9 @@ import arrow.core.Either
import arrow.core.raise.either
import com.squareup.moshi.Moshi
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.visa.TangemPayStorage
@ -44,18 +46,19 @@ internal class TangemPayRequestPerformer @Inject constructor(
private val refreshTokensMutex = Mutex()
private var refreshTokensJob: Deferred<Either<UniversalError, VisaAuthTokens>>? = null
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> T): Either<UniversalError, T> = either {
withContext(dispatchers.io) {
performRequest(requestBlock = requestBlock, refreshTokens = ::refreshAuthTokens).bind()
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> ApiResponse<T>): Either<UniversalError, T> =
either {
withContext(dispatchers.io) {
performRequest(requestBlock = requestBlock, refreshTokens = ::refreshAuthTokens).bind()
}
}
}
private suspend fun <T : Any> performRequest(
requestBlock: suspend (header: String) -> T,
requestBlock: suspend (header: String) -> ApiResponse<T>,
refreshTokens: (suspend () -> Either<UniversalError, VisaAuthTokens>)? = null,
): Either<UniversalError, T> = either {
runCatching {
requestBlock("Bearer ${getAccessTokens().bind().accessToken}")
requestBlock("Bearer ${getAccessTokens().bind().accessToken}").getOrThrow()
}.getOrElse { error ->
when (error) {
is ApiResponseError.HttpException -> {

View file

@ -0,0 +1,51 @@
package com.tangem.data.visa.utils
import com.tangem.datasource.api.pay.models.response.TangemPayTxHistoryResponse
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.utils.converter.Converter
internal object TangemPayTxHistoryItemConverter :
Converter<TangemPayTxHistoryResponse.Transaction, TangemPayTxHistoryItem> {
@Suppress("CyclomaticComplexMethod")
override fun convert(value: TangemPayTxHistoryResponse.Transaction): TangemPayTxHistoryItem {
val spend = value.spend
val collateral = value.collateral
val payment = value.payment
val fee = value.fee
return TangemPayTxHistoryItem(
id = value.id,
date = when {
spend != null -> spend.postedAt
collateral != null -> collateral.postedAt
payment != null -> payment.postedAt
fee != null -> fee.postedAt
else -> null
},
amount = when {
spend != null -> spend.amount
collateral != null -> collateral.amount
payment != null -> payment.amount
fee != null -> fee.amount
else -> null
},
merchantName = when {
spend != null -> spend.merchantName
else -> null
},
status = when {
spend != null -> spend.status
payment != null -> payment.status
else -> null
},
currency = when {
spend != null -> spend.currency
collateral != null -> collateral.currency
payment != null -> payment.currency
fee != null -> fee.currency
else -> null
},
)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.visa.model
import org.joda.time.DateTime
import java.math.BigDecimal
data class TangemPayTxHistoryItem(
val id: String,
val date: DateTime?,
val amount: BigDecimal?,
val merchantName: String?,
val status: String?,
val currency: String?,
) {
val timeStampInMillis: Long = date?.millis ?: 0
}

View file

@ -1,9 +1,9 @@
package com.tangem.domain.tangempay.model
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.pagination.BatchFlow
import com.tangem.pagination.BatchingContext
typealias TangemPayTxHistoryListBatchingContext = BatchingContext<Int, TangemPayTxHistoryListConfig, Nothing>
typealias TangemPayTxHistoryListBatchFlow = BatchFlow<Int, List<VisaTxHistoryItem>, Nothing>
typealias TangemPayTxHistoryListBatchFlow = BatchFlow<Int, List<TangemPayTxHistoryItem>, Nothing>

View file

@ -6,35 +6,34 @@ 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.domain.visa.model.TangemPayTxHistoryItem
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
import java.util.Currency
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,
)
) : Converter<TangemPayTxHistoryItem, TransactionState> {
override fun convert(value: TangemPayTxHistoryItem): TransactionState {
val localDate = value.date?.withZone(DateTimeZone.getDefault())
val currency = Currency.getInstance(value.currency)
val amount = value.amount.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
}
return TransactionState.Content(
txHash = value.id,
amount = "${StringsSigns.MINUS}$fiatAmount",
time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter),
amount = "${StringsSigns.MINUS}$amount",
time = localDate?.let { DateTimeFormatters.formatDate(it, 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,
timestamp = localDate?.millis ?: 0,
onClick = { txHistoryUiActions.openTxInExplorer(value.id) },
)
}

View file

@ -4,7 +4,7 @@ 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.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.utils.TxHistoryUiActions
import com.tangem.pagination.BatchAction
@ -69,7 +69,7 @@ internal class TangemPayTxHistoryListManager(
)
}
private fun updateState(batchListState: BatchListState<Int, List<VisaTxHistoryItem>>) {
private fun updateState(batchListState: BatchListState<Int, List<TangemPayTxHistoryItem>>) {
state.update { state ->
val clearUiBatches =
state.status is PaginationStatus.InitialLoading && batchListState.status is PaginationStatus.Paginating

View file

@ -1,7 +1,7 @@
package com.tangem.features.tangempay.utils
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryItemsConverter
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.utils.TxHistoryUiActions
@ -10,11 +10,7 @@ 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 kotlinx.coroutines.flow.*
import java.util.UUID
internal class TangemPayTxHistoryUiManager(
@ -40,7 +36,7 @@ internal class TangemPayTxHistoryUiManager(
private val txHistoryItemConverter = TangemPayTxHistoryItemsConverter(txHistoryUiActions = txHistoryUiActions)
fun createOrUpdateUiBatches(
newCurrencyBatches: List<Batch<Int, List<VisaTxHistoryItem>>>,
newCurrencyBatches: List<Batch<Int, List<TangemPayTxHistoryItem>>>,
clearUiBatches: Boolean,
): List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> {
val currentUiBatches = state.value.uiBatches
@ -72,7 +68,7 @@ internal class TangemPayTxHistoryUiManager(
return batches
}
private fun generateUiItems(key: Int, data: List<VisaTxHistoryItem>): List<TxHistoryUM.TxHistoryItemUM> {
private fun generateUiItems(key: Int, data: List<TangemPayTxHistoryItem>): List<TxHistoryUM.TxHistoryItemUM> {
val items = mutableListOf<TxHistoryUM.TxHistoryItemUM>()
// Add title for the first batch
@ -115,7 +111,7 @@ internal class TangemPayTxHistoryUiManager(
}
private fun List<TxHistoryUM.TxHistoryItemUM>.transactionItemsSizeNotEqual(
txInfos: List<VisaTxHistoryItem>,
txInfos: List<TangemPayTxHistoryItem>,
): Boolean {
return this.filterIsInstance<TxHistoryUM.TxHistoryItemUM.Transaction>().size != txInfos.size
}