Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-05 17:46:12 +05:00
parent 98fee821fa
commit 5ee6f98c3e
8 changed files with 88 additions and 32 deletions

View file

@ -6,6 +6,7 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.domain.onramp.model.OnrampAmount
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
internal class OnrampQuotesErrorConverter(
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
@ -35,7 +36,12 @@ internal class OnrampQuotesErrorConverter(
return OnrampQuote.Error.AmountTooSmallError(
paymentMethod = input.paymentMethod,
provider = input.provider,
amount = createFromAmountWithOffset(
fromAmount = OnrampAmount(
symbol = input.amount.currencySymbol,
value = input.amount.value.orZero(),
decimals = input.amount.decimals,
),
requiredAmount = createFromAmountWithOffset(
amountWithOffset = minAmount,
decimals = decimals,
symbol = input.amount.currencySymbol,
@ -53,7 +59,12 @@ internal class OnrampQuotesErrorConverter(
return OnrampQuote.Error.AmountTooBigError(
paymentMethod = input.paymentMethod,
provider = input.provider,
amount = createFromAmountWithOffset(
fromAmount = OnrampAmount(
symbol = input.amount.currencySymbol,
value = input.amount.value.orZero(),
decimals = input.amount.decimals,
),
requiredAmount = createFromAmountWithOffset(
amountWithOffset = maxAmount,
decimals = decimals,
symbol = input.amount.currencySymbol,

View file

@ -21,7 +21,16 @@ sealed interface OnrampProviderWithQuote {
val availablePaymentMethods: List<OnrampPaymentMethod>,
) : Unavailable
data class AvailableFrom(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable
data class AvailableUpTo(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable
data class AvailableFrom(
override val provider: OnrampProvider,
val fromAmount: OnrampAmount,
val requiredAmount: OnrampAmount,
) : Unavailable
data class AvailableUpTo(
override val provider: OnrampProvider,
val fromAmount: OnrampAmount,
val requiredAmount: OnrampAmount,
) : Unavailable
}
}

View file

@ -19,13 +19,15 @@ sealed class OnrampQuote {
data class AmountTooSmallError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val amount: OnrampAmount,
val fromAmount: OnrampAmount,
val requiredAmount: OnrampAmount,
) : Error()
data class AmountTooBigError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val amount: OnrampAmount,
val fromAmount: OnrampAmount,
val requiredAmount: OnrampAmount,
) : Error()
}
}

View file

@ -54,11 +54,13 @@ class GetOnrampProviderWithQuoteUseCase(private val repository: OnrampRepository
when (amountError) {
is OnrampQuote.Error.AmountTooBigError -> Unavailable.AvailableUpTo(
provider = amountError.provider,
amount = amountError.amount,
fromAmount = amountError.fromAmount,
requiredAmount = amountError.requiredAmount,
)
is OnrampQuote.Error.AmountTooSmallError -> Unavailable.AvailableFrom(
provider = amountError.provider,
amount = amountError.amount,
fromAmount = amountError.fromAmount,
requiredAmount = amountError.requiredAmount,
)
}
}

View file

@ -4,12 +4,12 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.math.BigDecimal
class GetOnrampQuotesUseCase(
private val settingsRepository: SettingsRepository,
@ -24,15 +24,29 @@ class GetOnrampQuotesUseCase(
quotes.groupBy { it.paymentMethod.type }
.asSequence()
.sortedBy { it.key.getPriority(isGooglePayAvailable) }
.map { grouped ->
grouped.value.sortedByDescending {
(it as? OnrampQuote.Data)?.toAmount?.value ?: BigDecimal.ZERO
}
}
.sortByRate()
.toList()
.flatten()
.right()
}
.catch { emit(it.left()) }
}
/**
* Sorting providers by rule:
*
* 1. Highest rate
* 2. Smallest difference between entered amount and required min/max amount
*/
private fun Sequence<Map.Entry<PaymentMethodType, List<OnrampQuote>>>.sortByRate() = map { grouped ->
grouped.value.sortedByDescending {
when (it) {
is OnrampQuote.Data -> it.toAmount.value
// negative difference to sort both when data and unavailable is present
is OnrampQuote.Error.AmountTooSmallError -> it.fromAmount.value - it.requiredAmount.value
is OnrampQuote.Error.AmountTooBigError -> it.requiredAmount.value - it.fromAmount.value
}
}
}
}

View file

@ -60,7 +60,7 @@ internal class OnrampAmountStateFactory(
)
}
fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM {
fun getAmountSecondaryUpdatedState(quote: OnrampQuote, isBestRate: Boolean): OnrampMainComponentUM {
val currentState = currentStateProvider()
if (currentState !is OnrampMainComponentUM.Content) return currentState
@ -71,7 +71,7 @@ internal class OnrampAmountStateFactory(
amountBlockState = amountState.copy(
secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState),
),
providerBlockState = quote.toProviderBlockState(),
providerBlockState = quote.toProviderBlockState(isBestRate),
buyButtonConfig = currentState.buyButtonConfig.copy(
enabled = quote is OnrampQuote.Data,
onClick = {
@ -118,11 +118,11 @@ internal class OnrampAmountStateFactory(
)
}
private fun OnrampQuote.toProviderBlockState(): OnrampProviderBlockUM {
private fun OnrampQuote.toProviderBlockState(isBestRate: Boolean): OnrampProviderBlockUM {
return OnrampProviderBlockUM.Content(
paymentMethod = paymentMethod,
providerName = provider.info.name,
isBestRate = true,
isBestRate = isBestRate,
onClick = onrampIntents::openProviders,
)
}
@ -136,7 +136,7 @@ internal class OnrampAmountStateFactory(
OnrampAmountSecondaryFieldUM.Content(stringReference(amount))
}
is OnrampQuote.Error.AmountTooBigError -> {
val amount = this.amount.value.format {
val amount = requiredAmount.value.format {
fiat(
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol,
@ -150,7 +150,7 @@ internal class OnrampAmountStateFactory(
)
}
is OnrampQuote.Error.AmountTooSmallError -> {
val amount = this.amount.value.format {
val amount = requiredAmount.value.format {
fiat(
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol,

View file

@ -177,7 +177,9 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun subscribeToQuotesUpdate() {
getOnrampQuotesUseCase.invoke()
.onEach { maybeQuotes ->
val quote = maybeQuotes.getOrNull()?.firstOrNull() ?: return@onEach
val quotes = maybeQuotes.getOrNull() ?: return@onEach
val isNoQuotes = quotes.none { it is OnrampQuote.Data }
val quote = quotes.firstOrNull() ?: return@onEach
if (quote is OnrampQuote.Data && lastAmount.value != quote.fromAmount.value) {
lastAmount.value = quote.fromAmount.value
analyticsEventHandler.send(
@ -188,7 +190,7 @@ internal class OnrampMainComponentModel @Inject constructor(
),
)
}
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote) }
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote, isNoQuotes) }
}
.launchIn(modelScope)
}

View file

@ -32,7 +32,6 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@ -125,11 +124,11 @@ internal class SelectProviderModel @Inject constructor(
}
private fun List<OnrampProviderWithQuote>.toProvidersListItems(): ImmutableList<ProviderListItemUM> {
val sortedByRate = sortedByDescending {
(it as? OnrampProviderWithQuote.Data)?.toAmount?.value ?: BigDecimal.ZERO
}
val bestProvider = sortedByRate.firstOrNull()
return sortedByRate.mapIndexed { index, quote ->
val sorted = sortByRate()
val isQuoteExist = any { it is OnrampProviderWithQuote.Data }
val bestProvider = sorted.firstOrNull().takeIf { isQuoteExist }
return sorted.mapIndexed { index, quote ->
when (quote) {
is OnrampProviderWithQuote.Data -> {
val rate = quote.toAmount.value.format {
@ -154,8 +153,8 @@ internal class SelectProviderModel @Inject constructor(
)
}
is OnrampProviderWithQuote.Unavailable.AvailableFrom -> {
val amount = quote.amount.value.format {
crypto(symbol = quote.amount.symbol, decimals = quote.amount.decimals)
val amount = quote.requiredAmount.value.format {
crypto(symbol = quote.requiredAmount.symbol, decimals = quote.requiredAmount.decimals)
}
ProviderListItemUM.Unavailable(
providerId = quote.provider.id,
@ -165,8 +164,8 @@ internal class SelectProviderModel @Inject constructor(
)
}
is OnrampProviderWithQuote.Unavailable.AvailableUpTo -> {
val amount = quote.amount.value.format {
crypto(symbol = quote.amount.symbol, decimals = quote.amount.decimals)
val amount = quote.requiredAmount.value.format {
crypto(symbol = quote.requiredAmount.symbol, decimals = quote.requiredAmount.decimals)
}
ProviderListItemUM.Unavailable(
providerId = quote.provider.id,
@ -188,4 +187,21 @@ internal class SelectProviderModel @Inject constructor(
}
}.toImmutableList()
}
/**
* Sorting providers by rule:
*
* 1. Highest rate
* 2. Smallest difference between entered amount and required min/max amount
*/
private fun List<OnrampProviderWithQuote>.sortByRate() = sortedByDescending {
when (it) {
is OnrampProviderWithQuote.Data -> it.toAmount.value
// negative difference to sort both when data and unavailable is present
is OnrampProviderWithQuote.Unavailable.AvailableFrom -> it.fromAmount.value - it.requiredAmount.value
is OnrampProviderWithQuote.Unavailable.AvailableUpTo -> it.requiredAmount.value - it.fromAmount.value
is OnrampProviderWithQuote.Unavailable.NotSupportedPaymentMethod -> null
}
}
}