Updated on 2026-08-14
This commit is contained in:
parent
94af682f08
commit
a284c8d311
13 changed files with 236 additions and 29 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -5,4 +5,5 @@ interface OnrampIntents {
|
|||
fun openSettings()
|
||||
fun openCurrenciesList()
|
||||
fun onBuyClick()
|
||||
fun openProviders()
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ internal sealed interface OnrampMainComponentUM {
|
|||
override val topBarConfig: OnrampMainTopBarUM,
|
||||
override val buyButtonConfig: BuyButtonConfig,
|
||||
val amountBlockState: OnrampAmountBlockUM,
|
||||
val providerBlockState: OnrampProviderBlockUM,
|
||||
) : OnrampMainComponentUM
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ internal class OnrampStateFactory(
|
|||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
buyButtonConfig = state.buyButtonConfig,
|
||||
amountBlockState = getInitialAmountBlockState(currency),
|
||||
providerBlockState = OnrampProviderBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue