diff --git a/app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt index 61170463f0..2795fe2a8c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt @@ -1,18 +1,27 @@ package com.tangem.tap.di.domain import com.tangem.domain.visa.GetVisaCurrencyUseCase +import com.tangem.domain.visa.GetVisaTxHistoryUseCase import com.tangem.domain.visa.repository.VisaRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped @Module @InstallIn(ViewModelComponent::class) internal object VisaDomainModule { @Provides + @ViewModelScoped fun provideVisaCurrencyUseCase(visaRepository: VisaRepository): GetVisaCurrencyUseCase { return GetVisaCurrencyUseCase(visaRepository) } + + @Provides + @ViewModelScoped + fun provideGetVisaTxHistoryUseCase(visaRepository: VisaRepository): GetVisaTxHistoryUseCase { + return GetVisaTxHistoryUseCase(visaRepository) + } } \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 26f4c9abe7..d41cf5f9c7 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -33,6 +33,9 @@ dependencies { implementation(deps.arrow.core) implementation(deps.arrow.fx) implementation(deps.jodatime) + implementation(deps.timber) + implementation(deps.androidx.paging.runtime) + implementation(deps.moshi.kotlin) /** Libs - Tangem */ implementation(deps.tangem.blockchain) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 9ba7dd179b..e2e0bc5567 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -1,22 +1,30 @@ package com.tangem.data.visa +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData import arrow.fx.coroutines.parZip import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType import com.tangem.common.card.EllipticCurve +import com.tangem.common.extensions.toHexString import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.visa.utils.VisaConfig import com.tangem.data.visa.utils.VisaCurrencyFactory +import com.tangem.data.visa.utils.VisaTxHistoryPagingSource import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.domain.visa.repository.VisaRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.lib.visa.VisaContractInfoProvider +import com.tangem.lib.visa.api.VisaApi import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import java.math.BigDecimal @@ -24,6 +32,7 @@ import java.math.BigDecimal internal class DefaultVisaRepository( private val visaContractInfoProvider: VisaContractInfoProvider, private val tangemTechApi: TangemTechApi, + private val visaApi: VisaApi, private val cacheRegistry: CacheRegistry, private val userWalletsStore: UserWalletsStore, private val dispatchers: CoroutineDispatcherProvider, @@ -71,14 +80,34 @@ internal class DefaultVisaRepository( ) } - private suspend fun getFiatRate(): BigDecimal? { - val fiatCurrencyId = VisaConfig.fiatCurrency.code.lowercase() - val quotes = tangemTechApi.getQuotes( - currencyId = fiatCurrencyId, - coinIds = VisaConfig.TOKEN_ID, - ).getOrThrow() + override suspend fun getTxHistory( + userWalletId: UserWalletId, + pageSize: Int, + isRefresh: Boolean, + ): Flow> { + val userWallet = findVisaUserWallet(userWalletId) + val cardPubKey = getCardPubKey(userWallet).toHexString() + // val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing + val pager = Pager( + config = PagingConfig( + pageSize = pageSize, + initialLoadSize = pageSize, + ), + pagingSourceFactory = { + VisaTxHistoryPagingSource( + params = VisaTxHistoryPagingSource.Params( + cardPublicKey = cardPubKey, + pageSize = pageSize, + isRefresh = isRefresh, + ), + cacheRegistry = cacheRegistry, + visaApi = visaApi, + dispatchers = dispatchers, + ) + }, + ) - return quotes.quotes[VisaConfig.TOKEN_ID]?.price + return pager.flow } private suspend fun makeAddress(userWalletId: UserWalletId): String { @@ -91,6 +120,16 @@ internal class DefaultVisaRepository( } } + private suspend fun getFiatRate(): BigDecimal? { + val fiatCurrencyId = VisaConfig.fiatCurrency.code.lowercase() + val quotes = tangemTechApi.getQuotes( + currencyId = fiatCurrencyId, + coinIds = VisaConfig.TOKEN_ID, + ).getOrThrow() + + return quotes.quotes[VisaConfig.TOKEN_ID]?.price + } + private fun makeWalletAddresses(userWallet: UserWallet): Set
{ val walletBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index 4204ec5430..a0227da794 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -1,12 +1,15 @@ package com.tangem.data.visa.di +import com.squareup.moshi.Moshi import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.visa.BuildConfig import com.tangem.data.visa.DefaultVisaRepository import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.visa.repository.VisaRepository import com.tangem.lib.visa.VisaContractInfoProvider +import com.tangem.lib.visa.api.VisaApiBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -21,6 +24,7 @@ internal object VisaDataModule { @Provides @Singleton fun provideVisaRepository( + @NetworkMoshi moshi: Moshi, tangemTechApi: TangemTechApi, cacheRegistry: CacheRegistry, userWalletsStore: UserWalletsStore, @@ -30,10 +34,16 @@ internal object VisaDataModule { isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, dispatchers = dispatchers, ).build() + val visaApi = VisaApiBuilder( + useDevApi = true, + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + moshi = moshi, + ).build() return DefaultVisaRepository( contractInfoProvider, tangemTechApi, + visaApi, cacheRegistry, userWalletsStore, dispatchers, diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt new file mode 100644 index 0000000000..b664ad4320 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt @@ -0,0 +1,15 @@ +package com.tangem.data.visa.utils + +import android.os.Build +import timber.log.Timber +import java.util.Currency + +internal fun findCurrencyByNumericCode(code: Int): Currency { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Currency.getAvailableCurrencies().firstOrNull { it.numericCode == code } + ?: Currency.getInstance(VisaConfig.fiatCurrency.code) + } else { + Timber.w("Unable to get currency by numeric code on API level ${Build.VERSION.SDK_INT}") + Currency.getInstance(VisaConfig.fiatCurrency.code) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt new file mode 100644 index 0000000000..86d1439068 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt @@ -0,0 +1,19 @@ +package com.tangem.data.visa.utils + +import com.tangem.domain.visa.model.VisaTxHistoryItem +import com.tangem.lib.visa.model.VisaTxHistoryResponse + +internal class VisaTxHistoryItemFactory { + + fun create(transaction: VisaTxHistoryResponse.Transaction): VisaTxHistoryItem { + return VisaTxHistoryItem( + id = transaction.transactionId.toString(), + date = transaction.transactionDt, + amount = transaction.blockchainAmount, + fiatAmount = transaction.transactionAmount, + merchantName = transaction.merchantName, + status = transaction.transactionStatus, + fiatCurrency = findCurrencyByNumericCode(transaction.transactionCurrencyCode), + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt new file mode 100644 index 0000000000..1c54f97d8d --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt @@ -0,0 +1,100 @@ +package com.tangem.data.visa.utils + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.domain.visa.model.VisaTxHistoryItem +import com.tangem.lib.visa.api.VisaApi +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext +import timber.log.Timber + +internal class VisaTxHistoryPagingSource( + params: Params, + private val cacheRegistry: CacheRegistry, + private val visaApi: VisaApi, + private val dispatchers: CoroutineDispatcherProvider, +) : PagingSource() { + + private val itemsFactory = VisaTxHistoryItemFactory() + + private val cardPublicKey = params.cardPublicKey + private val pageSize = params.pageSize + private val isRefresh = params.isRefresh + + private val pagedItems = MutableStateFlow>>( + value = emptyMap(), + ) + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + + anchorPage?.prevKey?.plus(pageSize) ?: anchorPage?.nextKey?.minus(pageSize) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val offsetToLoad = params.key ?: INITIAL_OFFSET + + return try { + fetchItemsIfExpired(offsetToLoad, pageSize, isRefresh = isRefresh && params is LoadParams.Refresh) + + val items = pagedItems.value[offsetToLoad].orEmpty() + val prevOffset = when { + items.isEmpty() -> null + offsetToLoad > INITIAL_OFFSET -> offsetToLoad - pageSize + else -> null + } + val nextOffset = when { + items.isEmpty() -> INITIAL_OFFSET + items.size % pageSize == 0 -> offsetToLoad + pageSize + else -> null + } + + LoadResult.Page(items, prevOffset, nextOffset) + } catch (e: Throwable) { + Timber.e(e, "Unable to load the transaction history for the requested offset: $offsetToLoad") + LoadResult.Error(e) + } + } + + private suspend fun fetchItemsIfExpired(offset: Int, pageSize: Int, isRefresh: Boolean) { + cacheRegistry.invokeOnExpire( + key = getCacheKey(offset), + skipCache = isRefresh, + block = { fetchItems(offset, pageSize) }, + ) + } + + private suspend fun fetchItems(offset: Int, pageSize: Int) = withContext(dispatchers.io) { + val response = visaApi.getTxHistory( + cardPublicKey = cardPublicKey, + limit = pageSize, + offset = offset, + ).getOrThrow() + + pagedItems.update { + it.toMutableMap().apply { + this[offset] = response.transactions.map(itemsFactory::create) + } + } + } + + private fun getCacheKey(offset: Int): String { + return "visa_tx_history_${cardPublicKey}_$offset" + } + + class Params( + val cardPublicKey: String, + val pageSize: Int, + val isRefresh: Boolean, + ) + + private companion object { + const val INITIAL_OFFSET = 0 + } +} \ No newline at end of file diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 64262e878a..8032c2e995 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -19,4 +19,5 @@ dependencies { /** Libs - Other */ implementation(deps.jodatime) + implementation(deps.androidx.paging.runtime) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/GetVisaTxHistoryUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/GetVisaTxHistoryUseCase.kt new file mode 100644 index 0000000000..c14447313e --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/GetVisaTxHistoryUseCase.kt @@ -0,0 +1,32 @@ +package com.tangem.domain.visa + +import androidx.paging.PagingData +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.visa.model.VisaTxHistoryItem +import com.tangem.domain.visa.repository.VisaRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +class GetVisaTxHistoryUseCase( + private val visaRepository: VisaRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + pageSize: Int = DEFAULT_PAGE_SIZE, + isRefresh: Boolean = false, + ): Flow>> { + return visaRepository + .getTxHistory(userWalletId, pageSize, isRefresh) + .map, Either>> { it.right() } + .catch { emit(it.left()) } + } + + companion object { + const val DEFAULT_PAGE_SIZE = 10 + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaTxHistoryItem.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaTxHistoryItem.kt new file mode 100644 index 0000000000..6d3bcc876a --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaTxHistoryItem.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.visa.model + +import org.joda.time.DateTime +import java.math.BigDecimal +import java.util.Currency + +data class VisaTxHistoryItem( + val id: String, + val date: DateTime, + val amount: BigDecimal, + val fiatAmount: BigDecimal, + val merchantName: String?, + val status: String, + val fiatCurrency: Currency, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt index 96d6efd7fd..bd3860aa2b 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt @@ -1,9 +1,18 @@ package com.tangem.domain.visa.repository +import androidx.paging.PagingData import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow interface VisaRepository { suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean = false): VisaCurrency + + suspend fun getTxHistory( + userWalletId: UserWalletId, + pageSize: Int, + isRefresh: Boolean = false, + ): Flow> } \ No newline at end of file