Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-21 14:50:12 +05:00
parent defd947695
commit f53ffa9a64
14 changed files with 206 additions and 13 deletions

View file

@ -102,4 +102,10 @@ internal object OnrampDomainModule {
fun provideGetOnrampPaymentMethodsUseCase(onrampRepository: OnrampRepository): GetOnrampPaymentMethodsUseCase {
return GetOnrampPaymentMethodsUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideClearOnrampCacheUseCase(onrampRepository: OnrampRepository): ClearOnrampCacheUseCase {
return ClearOnrampCacheUseCase(onrampRepository)
}
}

View file

@ -1,8 +1,12 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.onramp.DefaultOnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.pairs.DefaultOnrampPairsStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.DefaultOnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.DefaultOnrampQuotesStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -18,4 +22,16 @@ internal object OnrampStoreModule {
fun provideOnrampPaymentMethodsStore(): OnrampPaymentMethodsStore {
return DefaultOnrampPaymentMethodsStore(dataStore = RuntimeDataStore())
}
@Provides
@Singleton
fun provideOnrampPairsStore(): OnrampPairsStore {
return DefaultOnrampPairsStore(dataStore = RuntimeDataStore())
}
@Provides
@Singleton
fun provideOnrampQuotesStore(): OnrampQuotesStore {
return DefaultOnrampQuotesStore(dataStore = RuntimeDataStore())
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.onramp.pairs
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
internal class DefaultOnrampPairsStore(
dataStore: StringKeyDataStore<List<OnrampPairDTO>>,
) : OnrampPairsStore, StringKeyDataStoreDecorator<String, List<OnrampPairDTO>>(dataStore) {
override fun provideStringKey(key: String): String = key
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.local.onramp.pairs
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
interface OnrampPairsStore {
suspend fun getSyncOrNull(key: String): List<OnrampPairDTO>?
suspend fun store(key: String, value: List<OnrampPairDTO>)
suspend fun clear()
}

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.local.onramp
package com.tangem.datasource.local.onramp.paymentmethods
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.datasource.local.datastore.core.StringKeyDataStore

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.local.onramp
package com.tangem.datasource.local.onramp.paymentmethods
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
@ -9,4 +9,6 @@ interface OnrampPaymentMethodsStore {
suspend fun store(key: String, value: List<PaymentMethodDTO>)
suspend fun contains(key: String): Boolean
suspend fun clear()
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.onramp.quotes
import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
internal class DefaultOnrampQuotesStore(
dataStore: StringKeyDataStore<List<OnrampQuoteResponse>>,
) : OnrampQuotesStore, StringKeyDataStoreDecorator<String, List<OnrampQuoteResponse>>(dataStore) {
override fun provideStringKey(key: String): String = key
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.local.onramp.quotes
import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse
interface OnrampQuotesStore {
suspend fun getSyncOrNull(key: String): List<OnrampQuoteResponse>?
suspend fun store(key: String, value: List<OnrampQuoteResponse>)
suspend fun clear()
}

View file

@ -3,13 +3,18 @@ package com.tangem.data.onramp
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
import com.tangem.data.onramp.converters.StatusConverter
import com.tangem.data.onramp.converters.PaymentMethodConverter
import com.tangem.data.onramp.converters.StatusConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.models.TangemExpressValues
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO
import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
@ -17,10 +22,13 @@ import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
@ -31,6 +39,8 @@ internal class DefaultOnrampRepository(
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
private val paymentMethodsStore: OnrampPaymentMethodsStore,
private val pairsStore: OnrampPairsStore,
private val quotesStore: OnrampQuotesStore,
) : OnrampRepository {
private val currencyConverter = CurrencyConverter()
@ -100,8 +110,8 @@ internal class DefaultOnrampRepository(
.map { it?.let(countryConverter::convert) }
}
override suspend fun fetchPaymentMethodsIfAbsent() {
if (paymentMethodsStore.contains(PAYMENT_METHODS_KEY)) return
override suspend fun fetchPaymentMethodsIfAbsent() = withContext(dispatchers.io) {
if (paymentMethodsStore.contains(PAYMENT_METHODS_KEY)) return@withContext
val response = safeApiCall(
call = { onrampApi.getPaymentMethods().bind() },
@ -113,6 +123,74 @@ internal class DefaultOnrampRepository(
paymentMethodsStore.store(PAYMENT_METHODS_KEY, response)
}
override suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency) =
withContext(dispatchers.io) {
val pairs = safeApiCall(
call = {
onrampApi.getPairs(
body = OnrampPairsRequest(
fromCurrencyCode = currency.code,
countryCode = country.code,
to = listOf(
OnrampDestinationDTO(
contractAddress = cryptoCurrency.getContractAddress(),
network = cryptoCurrency.network.backendId,
),
),
),
).bind()
},
onError = {
Timber.w(it, "Unable to fetch onramp pairs")
throw it
},
)
pairsStore.store(PAIRS_KEY, pairs)
}
override suspend fun fetchQuotes(
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
fromAmount: String,
) = withContext(dispatchers.io) {
val pairs = requireNotNull(pairsStore.getSyncOrNull(PAIRS_KEY)) {
"Unable to get pairs. At this point they must not be null."
}
val quotes = pairs.flatMap { pair ->
pair.providers.flatMap { provider ->
provider.paymentMethods.map { paymentMethod ->
async {
safeApiCall(
call = {
onrampApi.getQuote(
fromCurrencyCode = currency.code,
toContractAddress = cryptoCurrency.getContractAddress(),
toNetwork = cryptoCurrency.network.backendId,
paymentMethod = paymentMethod,
countryCode = country.code,
fromAmount = fromAmount,
toDecimals = cryptoCurrency.decimals,
providerId = provider.providerId,
).bind()
},
onError = {
Timber.w(
it,
"Unable to fetch onramp quotes for ${provider.providerId} -> $paymentMethod",
)
null
},
)
}
}
}
}
.awaitAll()
.filterNotNull()
quotesStore.store(QUOTES_KEY, quotes)
}
override suspend fun getPaymentMethods(): List<OnrampPaymentMethod> {
val paymentMethods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) {
"Onramp payment methods is absent in storage"
@ -120,7 +198,20 @@ internal class DefaultOnrampRepository(
return paymentMethodsConverter.convertList(paymentMethods)
}
override suspend fun clearCache() {
paymentMethodsStore.clear()
pairsStore.clear()
quotesStore.clear()
}
private fun CryptoCurrency.getContractAddress(): String = when (this) {
is CryptoCurrency.Coin -> TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
is CryptoCurrency.Token -> this.contractAddress
}
private companion object {
const val PAYMENT_METHODS_KEY = "onramp_payment_methods"
const val PAIRS_KEY = "onramp_pairs"
const val QUOTES_KEY = "onramp_quotes"
}
}

View file

@ -4,7 +4,9 @@ import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
@ -27,12 +29,16 @@ internal object OnrampDataModule {
dispatchers: CoroutineDispatcherProvider,
appPreferencesStore: AppPreferencesStore,
paymentMethodsStore: OnrampPaymentMethodsStore,
pairsStore: OnrampPairsStore,
quotesStore: OnrampQuotesStore,
): OnrampRepository {
return DefaultOnrampRepository(
onrampApi = onrampApi,
dispatchers = dispatchers,
appPreferencesStore = appPreferencesStore,
paymentMethodsStore = paymentMethodsStore,
pairsStore = pairsStore,
quotesStore = quotesStore,
)
}

View file

@ -3,10 +3,11 @@ package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampAvailability
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.CryptoCurrency
class CheckOnrampAvailabilityUseCase(private val repository: OnrampRepository) {
suspend operator fun invoke(): Either<Throwable, OnrampAvailability> {
suspend operator fun invoke(cryptoCurrency: CryptoCurrency): Either<Throwable, OnrampAvailability> {
return Either.catch {
repository.fetchPaymentMethodsIfAbsent()
repository.getDefaultCountrySync()?.let { savedCountry ->
@ -15,6 +16,11 @@ class CheckOnrampAvailabilityUseCase(private val repository: OnrampRepository) {
repository.saveDefaultCurrency(savedCountry.defaultCurrency)
savedCountry.defaultCurrency
}
repository.fetchPairs(
currency = currency,
country = savedCountry,
cryptoCurrency = cryptoCurrency,
)
OnrampAvailability.Available(country = savedCountry, currency = currency)
} else {
OnrampAvailability.NotSupported(savedCountry)

View file

@ -0,0 +1,10 @@
package com.tangem.domain.onramp
import com.tangem.domain.onramp.repositories.OnrampRepository
class ClearOnrampCacheUseCase(private val repository: OnrampRepository) {
suspend operator fun invoke() {
repository.clearCache()
}
}

View file

@ -2,8 +2,9 @@ package com.tangem.domain.onramp.repositories
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.tokens.model.CryptoCurrency
import kotlinx.coroutines.flow.Flow
interface OnrampRepository {
@ -13,6 +14,13 @@ interface OnrampRepository {
suspend fun getCountryByIp(): OnrampCountry
suspend fun getStatus(txId: String): OnrampStatus
suspend fun fetchPaymentMethodsIfAbsent()
suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency)
suspend fun fetchQuotes(
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
fromAmount: String,
)
// cache
suspend fun saveDefaultCurrency(currency: OnrampCurrency)
@ -22,4 +30,5 @@ interface OnrampRepository {
suspend fun getDefaultCountrySync(): OnrampCountry?
fun getDefaultCountry(): Flow<OnrampCountry?>
suspend fun getPaymentMethods(): List<OnrampPaymentMethod>
suspend fun clearCache()
}

View file

@ -6,6 +6,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.onramp.CheckOnrampAvailabilityUseCase
import com.tangem.domain.onramp.ClearOnrampCacheUseCase
import com.tangem.domain.onramp.GetOnrampCurrencyUseCase
import com.tangem.domain.onramp.model.OnrampAvailability
import com.tangem.domain.onramp.model.OnrampCurrency
@ -27,6 +28,7 @@ internal class OnrampMainComponentModel @Inject constructor(
private val router: Router,
private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase,
private val getOnrampCurrencyUseCase: GetOnrampCurrencyUseCase,
private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase,
paramsContainer: ParamsContainer,
) : Model(), OnrampIntents {
@ -53,7 +55,7 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun checkResidenceCountry() {
modelScope.launch {
checkOnrampAvailabilityUseCase.invoke()
checkOnrampAvailabilityUseCase.invoke(params.cryptoCurrency)
.onRight(::handleOnrampAvailability)
.onLeft { Timber.e(it) }
}
@ -99,4 +101,9 @@ internal class OnrampMainComponentModel @Inject constructor(
override fun openCurrenciesList() {
bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList)
}
override fun onDestroy() {
modelScope.launch { clearOnrampCacheUseCase.invoke() }
super.onDestroy()
}
}