Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-26 10:37:53 +05:00
parent 94af682f08
commit a284c8d311
13 changed files with 236 additions and 29 deletions

View file

@ -5,6 +5,7 @@ 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.OnrampQuoteErrorInput
import com.tangem.data.onramp.converters.error.OnrampQuotesErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
@ -192,18 +193,26 @@ internal class DefaultOnrampRepository(
toDecimals = cryptoCurrency.decimals,
providerId = provider.id,
).bind()
OnrampQuote.Content(
OnrampQuote.Data(
fromAmount = convertToBigDecimal(response.fromAmount),
toAmount = convertToBigDecimal(response.toAmount),
minFromAmount = convertToBigDecimal(response.minFromAmount),
maxFromAmount = convertToBigDecimal(response.maxFromAmount),
paymentMethodId = response.paymentMethod,
providerId = response.providerId,
paymentMethod = paymentMethod,
provider = provider,
)
null
},
onError = { error ->
if (error is ApiResponseError.HttpException) {
quotesErrorConverter.convert(error.errorBody.orEmpty() to amount)
quotesErrorConverter.convert(
OnrampQuoteErrorInput(
errorBody = error.errorBody.orEmpty(),
amount = amount,
paymentMethod = paymentMethod,
provider = provider,
),
)
} else {
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
null

View file

@ -0,0 +1,12 @@
package com.tangem.data.onramp.converters.error
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampProvider
import com.tangem.domain.tokens.model.Amount
data class OnrampQuoteErrorInput(
val errorBody: String,
val amount: Amount,
val paymentMethod: OnrampPaymentMethod,
val provider: OnrampProvider,
)

View file

@ -10,16 +10,15 @@ import com.tangem.utils.converter.Converter
internal class OnrampQuotesErrorConverter(
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
) : Converter<Pair<String, Amount>, OnrampQuote.Error?> {
) : Converter<OnrampQuoteErrorInput, OnrampQuote.Error?> {
@Suppress("MagicNumber")
override fun convert(value: Pair<String, Amount>): OnrampQuote.Error? {
val (errorResponse, amount) = value
override fun convert(value: OnrampQuoteErrorInput): OnrampQuote.Error? {
try {
val error = jsonAdapter.fromJson(errorResponse)?.error ?: return null
val error = jsonAdapter.fromJson(value.errorBody)?.error ?: return null
return when (error.code) {
2250 -> tryParseExchangeTooSmallAmountError(error = error, amount = amount)
2251 -> tryParseExchangeTooBigAmountError(error = error, amount = amount)
2250 -> tryParseExchangeTooSmallAmountError(error = error, input = value)
2251 -> tryParseExchangeTooBigAmountError(error = error, input = value)
else -> null
}
} catch (e: Exception) {
@ -29,34 +28,38 @@ internal class OnrampQuotesErrorConverter(
private fun tryParseExchangeTooSmallAmountError(
error: ExpressError,
amount: Amount,
input: OnrampQuoteErrorInput,
): OnrampQuote.Error.AmountTooSmallError? {
val minAmount = error.value?.minAmount ?: return null
val decimals = error.value?.decimals ?: return null
return OnrampQuote.Error.AmountTooSmallError(
paymentMethod = input.paymentMethod,
provider = input.provider,
amount = createFromAmountWithOffset(
amountWithOffset = minAmount,
decimals = decimals,
symbol = amount.currencySymbol,
type = amount.type,
symbol = input.amount.currencySymbol,
type = input.amount.type,
),
)
}
private fun tryParseExchangeTooBigAmountError(
error: ExpressError,
amount: Amount,
input: OnrampQuoteErrorInput,
): OnrampQuote.Error.AmountTooBigError? {
val maxAmount = error.value?.maxAmount ?: return null
val decimals = error.value?.decimals ?: return null
return OnrampQuote.Error.AmountTooBigError(
paymentMethod = input.paymentMethod,
provider = input.provider,
amount = createFromAmountWithOffset(
amountWithOffset = maxAmount,
decimals = decimals,
symbol = amount.currencySymbol,
type = amount.type,
symbol = input.amount.currencySymbol,
type = input.amount.type,
),
)
}

View file

@ -5,19 +5,30 @@ import java.math.BigDecimal
sealed class OnrampQuote {
data class Content(
abstract val paymentMethod: OnrampPaymentMethod
abstract val provider: OnrampProvider
data class Data(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
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 AmountTooSmallError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val amount: Amount,
) : Error()
data class AmountTooBigError(val amount: Amount) : Error()
data class AmountTooBigError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val amount: Amount,
) : Error()
}
}

View file

@ -5,4 +5,5 @@ interface OnrampIntents {
fun openSettings()
fun openCurrenciesList()
fun onBuyClick()
fun openProviders()
}

View file

@ -45,6 +45,7 @@ internal sealed interface OnrampMainComponentUM {
override val topBarConfig: OnrampMainTopBarUM,
override val buyButtonConfig: BuyButtonConfig,
val amountBlockState: OnrampAmountBlockUM,
val providerBlockState: OnrampProviderBlockUM,
) : OnrampMainComponentUM
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.onramp.main.entity
sealed class OnrampProviderBlockUM {
data object Empty : OnrampProviderBlockUM()
data object Loading : OnrampProviderBlockUM()
data class Content(
val paymentMethodIconUrl: String,
val paymentMethodName: String,
val providerName: String,
val isBestRate: Boolean,
val onClick: () -> Unit,
) : OnrampProviderBlockUM()
}

View file

@ -42,6 +42,7 @@ internal class OnrampStateFactory(
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
buyButtonConfig = state.buyButtonConfig,
amountBlockState = getInitialAmountBlockState(currency),
providerBlockState = OnrampProviderBlockUM.Empty,
)
}

View file

@ -6,6 +6,7 @@ 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.features.onramp.main.entity.OnrampProviderBlockUM
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
@ -39,6 +40,7 @@ internal class OnrampAmountFieldChangeConverter(
amountFieldModel = amountFieldModel,
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading,
),
providerBlockState = OnrampProviderBlockUM.Loading,
)
}
@ -59,6 +61,7 @@ internal class OnrampAmountFieldChangeConverter(
amountFieldModel = amountFieldModel,
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
),
providerBlockState = OnrampProviderBlockUM.Empty,
)
}
}

View file

@ -10,12 +10,13 @@ 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.features.onramp.main.entity.*
import com.tangem.utils.Provider
internal class OnrampAmountStateFactory(private val currentStateProvider: Provider<OnrampMainComponentUM>) {
internal class OnrampAmountStateFactory(
private val currentStateProvider: Provider<OnrampMainComponentUM>,
private val onrampIntents: OnrampIntents,
) {
private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter(
currentStateProvider = currentStateProvider,
@ -64,14 +65,25 @@ internal class OnrampAmountStateFactory(private val currentStateProvider: Provid
val amountState = currentState.amountBlockState
return currentState.copy(
amountBlockState = amountState.copy(
secondaryFieldModel = quote.toUiModel(amountState),
secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState),
),
providerBlockState = quote.toProviderBlockState(),
)
}
private fun OnrampQuote.toUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM {
private fun OnrampQuote.toProviderBlockState(): OnrampProviderBlockUM {
return OnrampProviderBlockUM.Content(
paymentMethodIconUrl = paymentMethod.imageUrl,
paymentMethodName = paymentMethod.name,
providerName = provider.info.name,
isBestRate = true,
onClick = onrampIntents::openProviders,
)
}
private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM {
return when (this) {
is OnrampQuote.Content -> {
is OnrampQuote.Data -> {
val amount = toAmount.format {
crypto(
symbol = amountState.amountFieldModel.cryptoAmount.currencySymbol,

View file

@ -41,7 +41,10 @@ internal class OnrampMainComponentModel @Inject constructor(
cryptoCurrency = params.cryptoCurrency,
onrampIntents = this,
)
private val amountStateFactory = OnrampAmountStateFactory(currentStateProvider = Provider { _state.value })
private val amountStateFactory = OnrampAmountStateFactory(
currentStateProvider = Provider { _state.value },
onrampIntents = this,
)
private val _state: MutableStateFlow<OnrampMainComponentUM> = MutableStateFlow(
value = stateFactory.getInitialState(
@ -130,6 +133,9 @@ internal class OnrampMainComponentModel @Inject constructor(
bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList)
}
override fun openProviders() {
}
override fun onDestroy() {
modelScope.launch { clearOnrampCacheUseCase.invoke() }
super.onDestroy()

View file

@ -93,8 +93,10 @@ private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = M
.verticalScroll(rememberScrollState())
.navigationBarsPadding()
.padding(bottom = TangemTheme.dimens.spacing76),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
OnrampAmountContent(state = state.amountBlockState)
OnrampProviderContent(state = state.providerBlockState, modifier = Modifier.fillMaxWidth())
}
}

View file

@ -0,0 +1,133 @@
package com.tangem.features.onramp.main.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.appendSpace
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
@Composable
internal fun OnrampProviderContent(state: OnrampProviderBlockUM, modifier: Modifier = Modifier) {
when (state) {
is OnrampProviderBlockUM.Empty -> Unit
is OnrampProviderBlockUM.Loading -> OnrampProviderLoading(modifier)
is OnrampProviderBlockUM.Content -> OnrampProviderBlock(modifier = modifier, state = state)
}
}
@Composable
private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
.background(TangemTheme.colors.background.action)
.clickable(onClick = state.onClick)
.padding(TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
SubcomposeAsyncImage(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12)
.size(size = TangemTheme.dimens.size40)
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(context = LocalContext.current)
.data(state.paymentMethodIconUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
contentDescription = null,
)
Column(modifier = Modifier.weight(1F)) {
Text(
text = buildAnnotatedString {
append(stringResource(id = R.string.onramp_pay_with))
appendSpace()
withStyle(
style = SpanStyle(
fontWeight = TangemTheme.typography.subtitle2.fontWeight,
color = TangemTheme.colors.text.primary1,
),
) {
append(state.paymentMethodName)
}
},
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
)
Text(
text = buildAnnotatedString {
append(stringResource(id = R.string.onramp_via))
appendSpace()
append(state.providerName)
},
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
Text(
modifier = Modifier
.background(
color = TangemTheme.colors.icon.accent,
shape = RoundedCornerShape(TangemTheme.dimens.radius4),
)
.padding(
horizontal = TangemTheme.dimens.spacing6,
vertical = TangemTheme.dimens.spacing1,
),
text = stringResource(id = R.string.express_provider_best_rate),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary2,
)
}
}
@Composable
private fun OnrampProviderLoading(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
.background(TangemTheme.colors.background.action)
.padding(TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
Text(
text = stringResource(id = R.string.express_provider),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
CircularProgressIndicator(
color = TangemTheme.colors.icon.informative,
strokeWidth = TangemTheme.dimens.size2,
modifier = Modifier.size(TangemTheme.dimens.size16),
)
Text(
text = stringResource(id = R.string.express_fetch_best_rates),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}