diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index b753ffc387..87a22537d3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -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 + @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 + @GET("v1/customer/kyc") suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt new file mode 100644 index 0000000000..c6e87268e6 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt @@ -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, + ) + + @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, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayTxHistoryItemsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayTxHistoryItemsStore.kt index 25a8c85188..1a6014d355 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayTxHistoryItemsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayTxHistoryItemsStore.kt @@ -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>>, + dataStore: StringKeyDataStore>>, ) : TangemPayTxHistoryItemsStore, - StringKeyDataStoreDecorator>>(dataStore) { + StringKeyDataStoreDecorator>>(dataStore) { override fun provideStringKey(key: UserWalletId): String = key.stringValue - override suspend fun getSyncOrNull(key: UserWalletId, offset: Int): List? { + override suspend fun getSyncOrNull(key: UserWalletId, cursor: String): List? { val storedValue = getSyncOrNull(key) - return storedValue?.get(offset) + return storedValue?.get(cursor) } - override suspend fun store(key: UserWalletId, offset: Int, value: List) { + override suspend fun store(key: UserWalletId, cursor: String, value: List) { val oldValue = getSyncOrNull(key).orEmpty() - val newValue = oldValue.toMutableMap().apply { put(offset, value) } + val newValue = oldValue.toMutableMap().apply { put(cursor, value) } store(key, newValue) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayTxHistoryItemsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayTxHistoryItemsStore.kt index 0d81b695f5..c327023e6b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayTxHistoryItemsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayTxHistoryItemsStore.kt @@ -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? + suspend fun getSyncOrNull(key: UserWalletId, cursor: String): List? suspend fun remove(key: UserWalletId) - suspend fun store(key: UserWalletId, offset: Int, value: List) + suspend fun store(key: UserWalletId, cursor: String, value: List) } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/CursorBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/CursorBatchFetcher.kt new file mode 100644 index 0000000000..e41f84d55d --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/CursorBatchFetcher.kt @@ -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( + private val prefetchDistance: Int, + private val batchSize: Int, + private val subFetcher: SubFetcher, + private val cursorFromItem: (TItem) -> String, +) : BatchFetcher> { + + data class Request( + val limit: Int, + val cursor: String?, + val params: TRequestParams, + ) + + fun interface SubFetcher { + suspend fun fetch( + request: Request, + lastResult: BatchFetchResult>?, + isFirstBatchFetching: Boolean, + ): BatchFetchResult> + } + + private val lastRequest = MutableStateFlow?>(null) + + override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult> { + 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>, + ): BatchFetchResult> { + 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 = + if (lastResult is BatchFetchResult.Success>) { + 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 + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt index 0230e63d63..1775e41665 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt @@ -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) } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 24eb2cbaa8..cd8d7634f2 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -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 = 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 = 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, ) } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt index 08447ba6a5..f104ae6725 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt @@ -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> { - return LimitOffsetBatchFetcher( + ): BatchFetcher> { + 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 { + ): List { 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, - ) } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index 943480ef0e..dabdf56d8b 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -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>? = null - suspend fun request(requestBlock: suspend (header: String) -> T): Either = either { - withContext(dispatchers.io) { - performRequest(requestBlock = requestBlock, refreshTokens = ::refreshAuthTokens).bind() + suspend fun request(requestBlock: suspend (header: String) -> ApiResponse): Either = + either { + withContext(dispatchers.io) { + performRequest(requestBlock = requestBlock, refreshTokens = ::refreshAuthTokens).bind() + } } - } private suspend fun performRequest( - requestBlock: suspend (header: String) -> T, + requestBlock: suspend (header: String) -> ApiResponse, refreshTokens: (suspend () -> Either)? = null, ): Either = either { runCatching { - requestBlock("Bearer ${getAccessTokens().bind().accessToken}") + requestBlock("Bearer ${getAccessTokens().bind().accessToken}").getOrThrow() }.getOrElse { error -> when (error) { is ApiResponseError.HttpException -> { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt new file mode 100644 index 0000000000..9c4257091e --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt @@ -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 { + + @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 + }, + ) + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt new file mode 100644 index 0000000000..2e97128461 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt @@ -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 +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryTypeAliases.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryTypeAliases.kt index 6a830fffdc..5f3c7c4d50 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryTypeAliases.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryTypeAliases.kt @@ -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 -typealias TangemPayTxHistoryListBatchFlow = BatchFlow, Nothing> \ No newline at end of file +typealias TangemPayTxHistoryListBatchFlow = BatchFlow, Nothing> \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt index d81befd5b0..3e07907e7b 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt @@ -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 { - 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 { + 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) }, ) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt index 67d9ff9635..261dbcc875 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt @@ -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>) { + private fun updateState(batchListState: BatchListState>) { state.update { state -> val clearUiBatches = state.status is PaginationStatus.InitialLoading && batchListState.status is PaginationStatus.Paginating diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt index 316815be75..fc3dc5f1df 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt @@ -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>>, + newCurrencyBatches: List>>, clearUiBatches: Boolean, ): List>> { val currentUiBatches = state.value.uiBatches @@ -72,7 +68,7 @@ internal class TangemPayTxHistoryUiManager( return batches } - private fun generateUiItems(key: Int, data: List): List { + private fun generateUiItems(key: Int, data: List): List { val items = mutableListOf() // Add title for the first batch @@ -115,7 +111,7 @@ internal class TangemPayTxHistoryUiManager( } private fun List.transactionItemsSizeNotEqual( - txInfos: List, + txInfos: List, ): Boolean { return this.filterIsInstance().size != txInfos.size }