Updated on 2026-08-14
This commit is contained in:
parent
6d3a1d6e89
commit
99a8c6623b
15 changed files with 328 additions and 171 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 item’s 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue