Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-25 14:14:58 +05:00
parent 44231e988d
commit e72c2c87bc
28 changed files with 426 additions and 105 deletions

View file

@ -108,4 +108,16 @@ internal object OnrampDomainModule {
fun provideClearOnrampCacheUseCase(onrampRepository: OnrampRepository): ClearOnrampCacheUseCase {
return ClearOnrampCacheUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampFetchQuotesUseCase(onrampRepository: OnrampRepository): OnrampFetchQuotesUseCase {
return OnrampFetchQuotesUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampQuotesUseCase(onrampRepository: OnrampRepository): GetOnrampQuotesUseCase {
return GetOnrampQuotesUseCase(onrampRepository)
}
}

View file

@ -27,6 +27,7 @@ dependencies {
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.onramp.models)
/** Tangem libraries */
implementation(deps.tangem.blockchain)

View file

@ -41,4 +41,7 @@ enum class ExchangeProviderType {
@Json(name = "dex-bridge")
DEX_BRIDGE,
@Json(name = "onramp")
ONRAMP,
}

View file

@ -1,11 +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
import com.tangem.domain.onramp.model.OnrampPair
internal class DefaultOnrampPairsStore(
dataStore: StringKeyDataStore<List<OnrampPairDTO>>,
) : OnrampPairsStore, StringKeyDataStoreDecorator<String, List<OnrampPairDTO>>(dataStore) {
dataStore: StringKeyDataStore<List<OnrampPair>>,
) : OnrampPairsStore, StringKeyDataStoreDecorator<String, List<OnrampPair>>(dataStore) {
override fun provideStringKey(key: String): String = key
}

View file

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

View file

@ -1,11 +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
import com.tangem.domain.onramp.model.OnrampQuote
internal class DefaultOnrampQuotesStore(
dataStore: StringKeyDataStore<List<OnrampQuoteResponse>>,
) : OnrampQuotesStore, StringKeyDataStoreDecorator<String, List<OnrampQuoteResponse>>(dataStore) {
dataStore: StringKeyDataStore<List<OnrampQuote>>,
) : OnrampQuotesStore, StringKeyDataStoreDecorator<String, List<OnrampQuote>>(dataStore) {
override fun provideStringKey(key: String): String = key
}

View file

@ -1,9 +1,11 @@
package com.tangem.datasource.local.onramp.quotes
import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse
import com.tangem.domain.onramp.model.OnrampQuote
import kotlinx.coroutines.flow.Flow
interface OnrampQuotesStore {
suspend fun getSyncOrNull(key: String): List<OnrampQuoteResponse>?
suspend fun store(key: String, value: List<OnrampQuoteResponse>)
suspend fun getSyncOrNull(key: String): List<OnrampQuote>?
fun get(key: String): Flow<List<OnrampQuote>>
suspend fun store(key: String, value: List<OnrampQuote>)
suspend fun clear()
}

View file

@ -1,19 +1,12 @@
package com.tangem.data.onramp
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
// TODO: [REDACTED_JIRA]
internal class DefaultOnrampErrorResolver : OnrampErrorResolver {
private val onrampErrorConverter = OnrampErrorConverter()
override fun resolve(throwable: Throwable): OnrampError {
return if (throwable is ApiResponseError.HttpException) {
onrampErrorConverter.convert(throwable.errorBody.orEmpty())
} else {
OnrampError.UnknownError
}
return OnrampError.UnknownError
}
}

View file

@ -5,13 +5,19 @@ import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
import com.tangem.data.onramp.converters.PaymentMethodConverter
import com.tangem.data.onramp.converters.StatusConverter
import com.tangem.data.onramp.converters.error.OnrampQuotesErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.TangemExpressValues
import com.tangem.datasource.api.express.models.response.ExchangeProvider
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.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
@ -20,27 +26,30 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
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.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.*
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
@Suppress("LongParameterList")
internal class DefaultOnrampRepository(
private val onrampApi: OnrampApi,
private val expressApi: TangemExpressApi,
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
private val paymentMethodsStore: OnrampPaymentMethodsStore,
private val pairsStore: OnrampPairsStore,
private val quotesStore: OnrampQuotesStore,
private val quotesErrorConverter: OnrampQuotesErrorConverter,
) : OnrampRepository {
private val currencyConverter = CurrencyConverter()
@ -120,77 +129,98 @@ internal class DefaultOnrampRepository(
throw it
},
)
paymentMethodsStore.store(PAYMENT_METHODS_KEY, response)
paymentMethodsStore.store(PAYMENT_METHODS_KEY, response.removeApplePay())
}
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,
val onrampPairs = async {
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)
).bind()
},
onError = {
Timber.w(it, "Unable to fetch onramp pairs")
throw it
},
)
}
val providers = async {
safeApiCall(
call = { expressApi.getProviders().bind() },
onError = {
Timber.w(it, "Unable to fetch express providers")
throw it
},
)
}
storeOnrampPairs(pairs = onrampPairs.await(), providers = providers.await())
}
override suspend fun fetchQuotes(
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
fromAmount: String,
) = withContext(dispatchers.io) {
override suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount) = 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 ->
val amountValue = requireNotNull(amount.value) { "Amount value must not be null" }
val fromAmount = amountValue.movePointRight(amount.decimals).toString()
val currency = requireNotNull(getDefaultCurrencySync()) { "Default currency must not be null" }
val country = requireNotNull(getDefaultCountrySync()) { "Default country must not be null" }
val quotes: List<OnrampQuote> = pairs.flatMap { pair ->
pair.providers.flatMap { provider ->
provider.paymentMethods.map { paymentMethod ->
async {
safeApiCall(
call = {
onrampApi.getQuote(
val response = onrampApi.getQuote(
fromCurrencyCode = currency.code,
toContractAddress = cryptoCurrency.getContractAddress(),
toNetwork = cryptoCurrency.network.backendId,
paymentMethod = paymentMethod,
paymentMethod = paymentMethod.id,
countryCode = country.code,
fromAmount = fromAmount,
toDecimals = cryptoCurrency.decimals,
providerId = provider.providerId,
providerId = provider.id,
).bind()
},
onError = {
Timber.w(
it,
"Unable to fetch onramp quotes for ${provider.providerId} -> $paymentMethod",
OnrampQuote.Content(
fromAmount = convertToBigDecimal(response.fromAmount),
toAmount = convertToBigDecimal(response.toAmount),
minFromAmount = convertToBigDecimal(response.minFromAmount),
maxFromAmount = convertToBigDecimal(response.maxFromAmount),
paymentMethodId = response.paymentMethod,
providerId = response.providerId,
)
null
},
onError = { error ->
if (error is ApiResponseError.HttpException) {
quotesErrorConverter.convert(error.errorBody.orEmpty() to amount)
} else {
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
null
}
},
)
}
}
}
}
.awaitAll()
.filterNotNull()
}.awaitAll().filterNotNull()
quotesStore.store(QUOTES_KEY, quotes)
}
override fun getQuotes(): Flow<List<OnrampQuote>> {
return quotesStore.get(QUOTES_KEY)
}
override suspend fun getPaymentMethods(): List<OnrampPaymentMethod> {
val paymentMethods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) {
"Onramp payment methods is absent in storage"
@ -198,17 +228,43 @@ internal class DefaultOnrampRepository(
return paymentMethodsConverter.convertList(paymentMethods)
}
override suspend fun clearCache() {
override suspend fun clearCache() = withContext(NonCancellable) {
paymentMethodsStore.clear()
pairsStore.clear()
quotesStore.clear()
}
private suspend fun storeOnrampPairs(pairs: List<OnrampPairDTO>, providers: List<ExchangeProvider>) {
val onrampPaymentMethods = getPaymentMethods()
val onrampPairs = pairs.map { pair ->
val onrampProviders = pair.providers.mapNotNull { onrampProviderDTO ->
val providerInfo = providers.find { provider ->
provider.id == onrampProviderDTO.providerId
} ?: return@mapNotNull null
OnrampProvider(
id = onrampProviderDTO.providerId,
info = OnrampProviderInfo(name = providerInfo.name, imageLarge = providerInfo.imageLargeUrl),
paymentMethods = onrampPaymentMethods.filter { paymentMethod ->
onrampProviderDTO.paymentMethods.any { paymentMethod.id == it }
},
)
}
OnrampPair(onrampProviders)
}
pairsStore.store(PAIRS_KEY, onrampPairs)
}
private fun CryptoCurrency.getContractAddress(): String = when (this) {
is CryptoCurrency.Coin -> TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
is CryptoCurrency.Token -> this.contractAddress
}
private fun convertToBigDecimal(value: String): BigDecimal =
requireNotNull(value.toBigDecimalOrNull()) { "Can not parse $value to BigDecimal" }
private fun List<PaymentMethodDTO>.removeApplePay(): List<PaymentMethodDTO> = filterNot { it.id == "apple-pay" }
private companion object {
const val PAYMENT_METHODS_KEY = "onramp_payment_methods"
const val PAIRS_KEY = "onramp_pairs"

View file

@ -1,11 +0,0 @@
package com.tangem.data.onramp.converters.error
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.utils.converter.Converter
internal class OnrampErrorConverter : Converter<String, OnrampError> {
override fun convert(value: String): OnrampError {
return OnrampError.UnknownError
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.data.onramp.converters.error
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.api.express.models.response.ExpressError
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.utils.converter.Converter
internal class OnrampQuotesErrorConverter(
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
) : Converter<Pair<String, Amount>, OnrampQuote.Error?> {
@Suppress("MagicNumber")
override fun convert(value: Pair<String, Amount>): OnrampQuote.Error? {
val (errorResponse, amount) = value
try {
val error = jsonAdapter.fromJson(errorResponse)?.error ?: return null
return when (error.code) {
2250 -> tryParseExchangeTooSmallAmountError(error = error, amount = amount)
2251 -> tryParseExchangeTooBigAmountError(error = error, amount = amount)
else -> null
}
} catch (e: Exception) {
return null
}
}
private fun tryParseExchangeTooSmallAmountError(
error: ExpressError,
amount: Amount,
): OnrampQuote.Error.AmountTooSmallError? {
val minAmount = error.value?.minAmount ?: return null
val decimals = error.value?.decimals ?: return null
return OnrampQuote.Error.AmountTooSmallError(
amount = createFromAmountWithOffset(
amountWithOffset = minAmount,
decimals = decimals,
symbol = amount.currencySymbol,
type = amount.type,
),
)
}
private fun tryParseExchangeTooBigAmountError(
error: ExpressError,
amount: Amount,
): OnrampQuote.Error.AmountTooBigError? {
val maxAmount = error.value?.maxAmount ?: return null
val decimals = error.value?.decimals ?: return null
return OnrampQuote.Error.AmountTooBigError(
amount = createFromAmountWithOffset(
amountWithOffset = maxAmount,
decimals = decimals,
symbol = amount.currencySymbol,
type = amount.type,
),
)
}
private fun createFromAmountWithOffset(
amountWithOffset: String,
decimals: Int,
symbol: String,
type: AmountType,
): Amount {
return Amount(
value = requireNotNull(
amountWithOffset.toBigDecimalOrNull()?.movePointLeft(decimals),
) { "wrong amount format, use only digits" },
decimals = decimals,
currencySymbol = symbol,
type = type,
)
}
}

View file

@ -1,9 +1,14 @@
package com.tangem.data.onramp.di
import com.squareup.moshi.Moshi
import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
import com.tangem.data.onramp.converters.error.OnrampQuotesErrorConverter
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
@ -26,19 +31,23 @@ internal object OnrampDataModule {
@Singleton
fun provideOnrampRepository(
onrampApi: OnrampApi,
expressApi: TangemExpressApi,
dispatchers: CoroutineDispatcherProvider,
appPreferencesStore: AppPreferencesStore,
paymentMethodsStore: OnrampPaymentMethodsStore,
pairsStore: OnrampPairsStore,
quotesStore: OnrampQuotesStore,
quotesErrorConverter: OnrampQuotesErrorConverter,
): OnrampRepository {
return DefaultOnrampRepository(
onrampApi = onrampApi,
expressApi = expressApi,
dispatchers = dispatchers,
appPreferencesStore = appPreferencesStore,
paymentMethodsStore = paymentMethodsStore,
pairsStore = pairsStore,
quotesStore = quotesStore,
quotesErrorConverter = quotesErrorConverter,
)
}
@ -59,4 +68,11 @@ internal object OnrampDataModule {
fun provideOnrampErrorResolver(): OnrampErrorResolver {
return DefaultOnrampErrorResolver()
}
@Provides
@Singleton
internal fun provideErrorsConverter(@NetworkMoshi moshi: Moshi): OnrampQuotesErrorConverter {
val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java)
return OnrampQuotesErrorConverter(jsonAdapter)
}
}

View file

@ -0,0 +1,3 @@
package com.tangem.domain.onramp.model
data class OnrampPair(val providers: List<OnrampProvider>)

View file

@ -0,0 +1,9 @@
package com.tangem.domain.onramp.model
data class OnrampProvider(
val id: String,
val info: OnrampProviderInfo,
val paymentMethods: List<OnrampPaymentMethod>,
)
data class OnrampProviderInfo(val name: String, val imageLarge: String)

View file

@ -0,0 +1,23 @@
package com.tangem.domain.onramp.model
import com.tangem.domain.tokens.model.Amount
import java.math.BigDecimal
sealed class OnrampQuote {
data class Content(
val fromAmount: BigDecimal,
val toAmount: BigDecimal,
val minFromAmount: BigDecimal,
val maxFromAmount: BigDecimal,
val paymentMethodId: String,
val providerId: String,
) : OnrampQuote()
sealed class Error : OnrampQuote() {
data class AmountTooSmallError(val amount: Amount) : Error()
data class AmountTooBigError(val amount: Amount) : Error()
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.repositories.OnrampRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
class GetOnrampQuotesUseCase(private val repository: OnrampRepository) {
operator fun invoke(): Flow<Either<Throwable, List<OnrampQuote>>> {
return repository.getQuotes()
.map<List<OnrampQuote>, Either<Throwable, List<OnrampQuote>>> { it.right() }
.catch { emit(it.left()) }
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.onramp
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.CryptoCurrency
class OnrampFetchQuotesUseCase(private val repository: OnrampRepository) {
suspend operator fun invoke(amount: Amount, cryptoCurrency: CryptoCurrency) {
repository.fetchQuotes(
cryptoCurrency = cryptoCurrency,
amount = amount,
)
}
}

View file

@ -1,9 +1,7 @@
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.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.*
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.CryptoCurrency
import kotlinx.coroutines.flow.Flow
@ -15,12 +13,7 @@ interface OnrampRepository {
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,
)
suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount)
// cache
suspend fun saveDefaultCurrency(currency: OnrampCurrency)
@ -30,5 +23,6 @@ interface OnrampRepository {
suspend fun getDefaultCountrySync(): OnrampCountry?
fun getDefaultCountry(): Flow<OnrampCountry?>
suspend fun getPaymentMethods(): List<OnrampPaymentMethod>
fun getQuotes(): Flow<List<OnrampQuote>>
suspend fun clearCache()
}

View file

@ -72,7 +72,7 @@ internal class OnrampStateFactory(
isValuePasted = false,
onValuePastedTriggerDismiss = {},
),
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading,
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.onramp.main.entity.factory.amount
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
import com.tangem.utils.Provider
@ -56,7 +57,7 @@ internal class OnrampAmountFieldChangeConverter(
return copy(
amountBlockState = amountBlockState.copy(
amountFieldModel = amountFieldModel,
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading,
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
),
)
}

View file

@ -1,7 +1,17 @@
package com.tangem.features.onramp.main.entity.factory.amount
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.tokens.model.AmountType
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM
import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
import com.tangem.utils.Provider
@ -35,4 +45,69 @@ internal class OnrampAmountStateFactory(private val currentStateProvider: Provid
),
)
}
fun getAmountSecondaryLoadingState(): OnrampMainComponentUM {
val currentState = currentStateProvider()
if (currentState !is OnrampMainComponentUM.Content) return currentState
val amountState = currentState.amountBlockState
return currentState.copy(
amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading),
)
}
fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM {
val currentState = currentStateProvider()
if (currentState !is OnrampMainComponentUM.Content) return currentState
val amountState = currentState.amountBlockState
return currentState.copy(
amountBlockState = amountState.copy(
secondaryFieldModel = quote.toUiModel(amountState),
),
)
}
private fun OnrampQuote.toUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM {
return when (this) {
is OnrampQuote.Content -> {
val amount = toAmount.format {
crypto(
symbol = amountState.amountFieldModel.cryptoAmount.currencySymbol,
decimals = amountState.amountFieldModel.cryptoAmount.decimals,
)
}
OnrampAmountSecondaryFieldUM.Content(stringReference(amount))
}
is OnrampQuote.Error.AmountTooBigError -> {
val amount = this.amount.value.format {
fiat(
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol,
)
}
OnrampAmountSecondaryFieldUM.Error(
resourceReference(
R.string.onramp_max_amount_restriction,
wrappedList(amount),
),
)
}
is OnrampQuote.Error.AmountTooSmallError -> {
val amount = this.amount.value.format {
fiat(
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol,
)
}
OnrampAmountSecondaryFieldUM.Error(
resourceReference(
R.string.onramp_min_amount_restriction,
wrappedList(amount),
),
)
}
}
}
}

View file

@ -5,9 +5,7 @@ import com.arkivanov.decompose.router.slot.activate
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.*
import com.tangem.domain.onramp.model.OnrampAvailability
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.features.onramp.main.OnrampMainComponent
@ -16,6 +14,7 @@ import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory
import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory
import com.tangem.features.onramp.utils.InputManager
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
@ -23,12 +22,16 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
internal class OnrampMainComponentModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase,
private val getOnrampCurrencyUseCase: GetOnrampCurrencyUseCase,
private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase,
private val fetchQuotesUseCase: OnrampFetchQuotesUseCase,
private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase,
private val amountInputManager: InputManager,
paramsContainer: ParamsContainer,
) : Model(), OnrampIntents {
@ -51,6 +54,7 @@ internal class OnrampMainComponentModel @Inject constructor(
init {
checkResidenceCountry()
subscribeToAmountChanges()
}
private fun checkResidenceCountry() {
@ -73,6 +77,7 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun handleOnrampAvailable(currency: OnrampCurrency) {
_state.update { stateFactory.getReadyState(currency) }
subscribeToCurrencyUpdates()
subscribeToQuotesUpdate()
}
private fun subscribeToCurrencyUpdates() {
@ -86,8 +91,31 @@ internal class OnrampMainComponentModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeToAmountChanges() = modelScope.launch {
amountInputManager.query
.filter(String::isNotEmpty)
.collectLatest { _ ->
val content = state.value as? OnrampMainComponentUM.Content ?: return@collectLatest
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
fetchQuotesUseCase.invoke(
amount = content.amountBlockState.amountFieldModel.fiatAmount,
cryptoCurrency = params.cryptoCurrency,
)
}
}
private fun subscribeToQuotesUpdate() {
getOnrampQuotesUseCase.invoke()
.onEach { maybeQuotes ->
val quote = maybeQuotes.getOrNull()?.firstOrNull() ?: return@onEach
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote) }
}
.launchIn(modelScope)
}
override fun onAmountValueChanged(value: String) {
_state.update { amountStateFactory.getOnAmountValueChange(value) }
modelScope.launch { amountInputManager.update(value) }
}
override fun openSettings() {

View file

@ -16,7 +16,7 @@ import com.tangem.features.onramp.selectcountry.entity.CountryListUM
import com.tangem.features.onramp.selectcountry.entity.CountryListUMController
import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsLoadingTransformer
import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsTransformer
import com.tangem.features.onramp.utils.SearchManager
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -30,7 +30,7 @@ import javax.inject.Inject
@ComponentScoped
internal class OnrampSelectCountryModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val searchManager: SearchManager,
private val searchManager: InputManager,
private val getOnrampCountriesUseCase: GetOnrampCountriesUseCase,
private val saveDefaultCountryUseCase: OnrampSaveDefaultCountryUseCase,
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,

View file

@ -17,7 +17,7 @@ import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState
import com.tangem.features.onramp.selectcurrency.entity.CurrencyListController
import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsLoadingTransformer
import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsTransformer
import com.tangem.features.onramp.utils.SearchManager
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -31,7 +31,7 @@ import javax.inject.Inject
@ComponentScoped
internal class OnrampSelectCurrencyModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val searchManager: SearchManager,
private val searchManager: InputManager,
private val getOnrampCurrenciesUseCase: GetOnrampCurrenciesUseCase,
private val saveDefaultCurrencyUseCase: OnrampSaveDefaultCurrencyUseCase,
paramsContainer: ParamsContainer,

View file

@ -23,7 +23,7 @@ import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoa
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
import com.tangem.features.onramp.utils.SearchManager
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -37,7 +37,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
getTokenListUseCase: GetTokenListUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val tokenListUMController: TokenListUMController,
private val searchManager: SearchManager,
private val searchManager: InputManager,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getAvailablePairsUseCase: GetAvailablePairsUseCase,

View file

@ -20,7 +20,7 @@ import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
import com.tangem.features.onramp.utils.SearchManager
import com.tangem.features.onramp.utils.InputManager
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -33,7 +33,7 @@ internal class OnrampTokenListModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tokenListUMController: TokenListUMController,
private val searchManager: SearchManager,
private val searchManager: InputManager,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getTokenListUseCase: GetTokenListUseCase,

View file

@ -12,7 +12,7 @@ import javax.inject.Inject
*
[REDACTED_AUTHOR]
*/
internal class SearchManager @Inject constructor() {
internal class InputManager @Inject constructor() {
val query: Flow<String>
get() = _query

View file

@ -32,16 +32,17 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
}
return PairsWithProviders(
pairs = pairs,
allProviders = value.providers.map { convertLeastProvider(it) },
allProviders = value.providers.mapNotNull { convertLeastProvider(it) },
)
}
private fun convertLeastProvider(exchangeProvider: ExchangeProvider): SwapProvider {
private fun convertLeastProvider(exchangeProvider: ExchangeProvider): SwapProvider? {
val type = convertExchangeType(exchangeProvider.type) ?: return null
return SwapProvider(
providerId = exchangeProvider.id,
rateTypes = emptyList(),
name = exchangeProvider.name,
type = convertExchangeType(exchangeProvider.type),
type = type,
imageLarge = exchangeProvider.imageLargeUrl,
termsOfUse = exchangeProvider.termsOfUse,
privacyPolicy = exchangeProvider.privacyPolicy,
@ -55,11 +56,12 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
providerAdditional: Map<String, ExchangeProvider>,
): SwapPairProviderDomain? {
val additionalProvider = providerAdditional[swapPairProvider.providerId] ?: return null
val type = convertExchangeType(additionalProvider.type) ?: return null
return SwapPairProviderDomain(
providerId = swapPairProvider.providerId,
rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) },
name = additionalProvider.name,
type = convertExchangeType(additionalProvider.type),
type = type,
imageLarge = additionalProvider.imageLargeUrl,
termsOfUse = additionalProvider.termsOfUse,
privacyPolicy = additionalProvider.privacyPolicy,
@ -68,11 +70,12 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
)
}
private fun convertExchangeType(type: ExchangeProviderType): ExchangeProviderTypeDomain {
private fun convertExchangeType(type: ExchangeProviderType): ExchangeProviderTypeDomain? {
return when (type) {
ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX
ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX
ExchangeProviderType.DEX_BRIDGE -> ExchangeProviderTypeDomain.DEX_BRIDGE
ExchangeProviderType.ONRAMP -> null
}
}
}