From a284c8d31141d488bcdaf8b5d5daf13eee31c78f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Nov 2024 10:37:53 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../data/onramp/DefaultOnrampRepository.kt | 17 ++- .../converters/error/OnrampQuoteErrorInput.kt | 12 ++ .../error/OnrampQuotesErrorConverter.kt | 27 ++-- .../tangem/domain/onramp/model/OnrampQuote.kt | 21 ++- .../onramp/main/entity/OnrampIntents.kt | 1 + .../main/entity/OnrampMainComponentUM.kt | 1 + .../main/entity/OnrampProviderBlockUM.kt | 13 ++ .../main/entity/factory/OnrampStateFactory.kt | 1 + .../OnrampAmountFieldChangeConverter.kt | 3 + .../amount/OnrampAmountStateFactory.kt | 26 +++- .../main/model/OnrampMainComponentModel.kt | 8 +- .../main/ui/OnrampMainComponentContent.kt | 2 + .../onramp/main/ui/OnrampProviderContent.kt | 133 ++++++++++++++++++ 13 files changed, 236 insertions(+), 29 deletions(-) create mode 100644 data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuoteErrorInput.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index d2bcad9d24..e6d6c8e177 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -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 diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuoteErrorInput.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuoteErrorInput.kt new file mode 100644 index 0000000000..218eb846c4 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuoteErrorInput.kt @@ -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, +) \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuotesErrorConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuotesErrorConverter.kt index 37fbddf76e..d487e0a34c 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuotesErrorConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuotesErrorConverter.kt @@ -10,16 +10,15 @@ import com.tangem.utils.converter.Converter internal class OnrampQuotesErrorConverter( private val jsonAdapter: JsonAdapter, -) : Converter, OnrampQuote.Error?> { +) : Converter { @Suppress("MagicNumber") - override fun convert(value: Pair): 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, ), ) } diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt index f347b73e67..565aab17c5 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt @@ -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() } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt index 0b1002a3bd..bc8946cbb5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt @@ -5,4 +5,5 @@ interface OnrampIntents { fun openSettings() fun openCurrenciesList() fun onBuyClick() + fun openProviders() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index b909ee310e..c390cb9a7e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -45,6 +45,7 @@ internal sealed interface OnrampMainComponentUM { override val topBarConfig: OnrampMainTopBarUM, override val buyButtonConfig: BuyButtonConfig, val amountBlockState: OnrampAmountBlockUM, + val providerBlockState: OnrampProviderBlockUM, ) : OnrampMainComponentUM } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt new file mode 100644 index 0000000000..2d34e8e66d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt @@ -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() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 9b078e114c..b138b4f5a9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -42,6 +42,7 @@ internal class OnrampStateFactory( topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), buyButtonConfig = state.buyButtonConfig, amountBlockState = getInitialAmountBlockState(currency), + providerBlockState = OnrampProviderBlockUM.Empty, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt index 72e7abff4b..184b3a1dd3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt @@ -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, ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index 42980cd4d5..d1c630786f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -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) { +internal class OnrampAmountStateFactory( + private val currentStateProvider: Provider, + 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, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 65794c66af..f8d3afd97d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -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 = 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() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt index 14318c4511..15551bbb8d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt @@ -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()) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt new file mode 100644 index 0000000000..0ebdc14d95 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt @@ -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, + ) + } + } +} \ No newline at end of file