Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-06 14:41:42 +05:00
parent f941f6f1b9
commit 6b4ddb1845
10 changed files with 151 additions and 34 deletions

View file

@ -23,12 +23,16 @@ sealed interface OnrampProviderWithQuote {
data class AvailableFrom(
override val provider: OnrampProvider,
val paymentMethod: OnrampPaymentMethod,
val quoteError: OnrampQuote.Error,
val fromAmount: OnrampAmount,
val requiredAmount: OnrampAmount,
) : Unavailable
data class AvailableUpTo(
override val provider: OnrampProvider,
val paymentMethod: OnrampPaymentMethod,
val quoteError: OnrampQuote.Error,
val fromAmount: OnrampAmount,
val requiredAmount: OnrampAmount,
) : Unavailable

View file

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

View file

@ -12,6 +12,7 @@ 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.*
import com.tangem.features.onramp.providers.entity.SelectProviderResult
import com.tangem.utils.Provider
internal class OnrampAmountStateFactory(
@ -91,30 +92,47 @@ internal class OnrampAmountStateFactory(
}
fun getAmountSecondaryUpdatedState(
quoteWithProvider: OnrampProviderWithQuote.Data,
providerResult: SelectProviderResult,
isBestRate: Boolean,
): OnrampMainComponentUM {
val currentState = currentStateProvider()
if (currentState !is OnrampMainComponentUM.Content) return currentState
val amountState = currentState.amountBlockState
val amount = quoteWithProvider.toAmount.value.format {
crypto(symbol = quoteWithProvider.toAmount.symbol, decimals = quoteWithProvider.toAmount.decimals)
val secondaryField = when (providerResult) {
is SelectProviderResult.ProviderWithError -> {
providerResult.quoteError.toSecondaryFieldUiModel(amountState)
}
is SelectProviderResult.ProviderWithQuote -> {
val amount = providerResult.toAmount.value.format {
crypto(symbol = providerResult.toAmount.symbol, decimals = providerResult.toAmount.decimals)
}
OnrampAmountSecondaryFieldUM.Content(stringReference(amount))
}
}
return currentState.copy(
amountBlockState = amountState.copy(
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(stringReference(amount)),
),
amountBlockState = amountState.copy(secondaryFieldModel = secondaryField),
providerBlockState = OnrampProviderBlockUM.Content(
paymentMethod = quoteWithProvider.paymentMethod,
providerId = quoteWithProvider.provider.id,
providerName = quoteWithProvider.provider.info.name,
paymentMethod = providerResult.paymentMethod,
providerId = providerResult.provider.id,
providerName = providerResult.provider.info.name,
isBestRate = isBestRate,
onClick = onrampIntents::openProviders,
),
buyButtonConfig = currentState.buyButtonConfig.copy(
enabled = true,
onClick = { onrampIntents.onBuyClick(quoteWithProvider) },
enabled = providerResult is SelectProviderResult.ProviderWithQuote,
onClick = {
if (providerResult is SelectProviderResult.ProviderWithQuote) {
onrampIntents.onBuyClick(
OnrampProviderWithQuote.Data(
provider = providerResult.provider,
paymentMethod = providerResult.paymentMethod,
toAmount = providerResult.toAmount,
fromAmount = providerResult.fromAmount,
),
)
}
},
),
)
}
@ -137,6 +155,14 @@ internal class OnrampAmountStateFactory(
}
OnrampAmountSecondaryFieldUM.Content(stringReference(amount))
}
is OnrampQuote.Error -> this.toSecondaryFieldUiModel(amountState)
}
}
private fun OnrampQuote.Error.toSecondaryFieldUiModel(
amountState: OnrampAmountBlockUM,
): OnrampAmountSecondaryFieldUM.Error {
return when (this) {
is OnrampQuote.Error.AmountTooBigError -> {
val amount = requiredAmount.value.format {
fiat(

View file

@ -30,6 +30,7 @@ import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory
import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory
import com.tangem.features.onramp.providers.entity.SelectProviderResult
import com.tangem.features.onramp.utils.InputManager
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -104,8 +105,8 @@ internal class OnrampMainComponentModel @Inject constructor(
subscribeToQuotesUpdate()
}
fun onProviderSelected(providerWithQuote: OnrampProviderWithQuote.Data, isBestRate: Boolean) {
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(providerWithQuote, isBestRate) }
fun onProviderSelected(result: SelectProviderResult, isBestRate: Boolean) {
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(result, isBestRate) }
}
private fun checkResidenceCountry() {

View file

@ -3,13 +3,13 @@ package com.tangem.features.onramp.providers
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.onramp.providers.entity.SelectProviderResult
internal interface SelectProviderComponent : ComposableBottomSheetComponent {
data class Params(
val onProviderClick: (OnrampProviderWithQuote.Data, Boolean) -> Unit,
val onProviderClick: (result: SelectProviderResult, isBestRate: Boolean) -> Unit,
val onDismiss: () -> Unit,
val selectedPaymentMethod: OnrampPaymentMethod,
val selectedProviderId: String,

View file

@ -17,6 +17,14 @@ internal sealed interface ProviderListItemUM {
val onClick: () -> Unit,
) : ProviderListItemUM
data class AvailableWithError(
override val providerId: String,
val imageUrl: String,
val name: String,
val subtitle: TextReference,
val onClick: () -> Unit,
) : ProviderListItemUM
data class Unavailable(
override val providerId: String,
val imageUrl: String,

View file

@ -0,0 +1,25 @@
package com.tangem.features.onramp.providers.entity
import com.tangem.domain.onramp.model.OnrampAmount
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampProvider
import com.tangem.domain.onramp.model.OnrampQuote
sealed class SelectProviderResult {
abstract val paymentMethod: OnrampPaymentMethod
abstract val provider: OnrampProvider
data class ProviderWithQuote(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val fromAmount: OnrampAmount,
val toAmount: OnrampAmount,
) : SelectProviderResult()
data class ProviderWithError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val quoteError: OnrampQuote.Error,
) : SelectProviderResult()
}

View file

@ -28,6 +28,7 @@ import com.tangem.features.onramp.providers.entity.ProviderListItemUM
import com.tangem.features.onramp.providers.entity.ProviderListPaymentMethodUM
import com.tangem.features.onramp.providers.entity.ProviderListUM
import com.tangem.utils.StringsSigns.MINUS
import com.tangem.features.onramp.providers.entity.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -133,6 +134,7 @@ internal class SelectProviderModel @Inject constructor(
}
}
@Suppress("LongMethod")
private fun List<OnrampProviderWithQuote>.toProvidersListItems(): ImmutableList<ProviderListItemUM> {
val sorted = sortByRate()
@ -157,14 +159,15 @@ internal class SelectProviderModel @Inject constructor(
isBestRate = quote == bestProvider,
diffRate = stringReference("$MINUS${rateDiff.format { percent() }}"),
onClick = {
analyticsEventHandler.send(
OnrampAnalyticsEvent.OnProviderChosen(
providerName = quote.provider.info.name,
tokenSymbol = params.cryptoCurrency.symbol,
onProviderSelected(
result = SelectProviderResult.ProviderWithQuote(
paymentMethod = quote.paymentMethod,
provider = quote.provider,
fromAmount = quote.fromAmount,
toAmount = quote.toAmount,
),
isBestRate = bestProvider == quote,
)
params.onProviderClick(quote, bestProvider == quote)
params.onDismiss()
},
)
}
@ -172,22 +175,42 @@ internal class SelectProviderModel @Inject constructor(
val amount = quote.requiredAmount.value.format {
crypto(symbol = quote.requiredAmount.symbol, decimals = quote.requiredAmount.decimals)
}
ProviderListItemUM.Unavailable(
ProviderListItemUM.AvailableWithError(
providerId = quote.provider.id,
imageUrl = quote.provider.info.imageLarge,
name = quote.provider.info.name,
subtitle = resourceReference(R.string.express_provider_min_amount, wrappedList(amount)),
onClick = {
onProviderSelected(
result = SelectProviderResult.ProviderWithError(
paymentMethod = quote.paymentMethod,
provider = quote.provider,
quoteError = quote.quoteError,
),
isBestRate = bestProvider == quote,
)
},
)
}
is OnrampProviderWithQuote.Unavailable.AvailableUpTo -> {
val amount = quote.requiredAmount.value.format {
crypto(symbol = quote.requiredAmount.symbol, decimals = quote.requiredAmount.decimals)
}
ProviderListItemUM.Unavailable(
ProviderListItemUM.AvailableWithError(
providerId = quote.provider.id,
imageUrl = quote.provider.info.imageLarge,
name = quote.provider.info.name,
subtitle = resourceReference(R.string.express_provider_max_amount, wrappedList(amount)),
onClick = {
onProviderSelected(
result = SelectProviderResult.ProviderWithError(
paymentMethod = quote.paymentMethod,
provider = quote.provider,
quoteError = quote.quoteError,
),
isBestRate = bestProvider == quote,
)
},
)
}
is OnrampProviderWithQuote.Unavailable.NotSupportedPaymentMethod -> {
@ -195,8 +218,9 @@ internal class SelectProviderModel @Inject constructor(
providerId = quote.provider.id,
imageUrl = quote.provider.info.imageLarge,
name = quote.provider.info.name,
subtitle = stringReference(
"Available with ${quote.availablePaymentMethods.joinToString { it.name }}",
subtitle = resourceReference(
id = R.string.onramp_avaiable_with_payment_methods,
formatArgs = wrappedList(quote.availablePaymentMethods.joinToString { it.name }),
),
)
}
@ -204,6 +228,17 @@ internal class SelectProviderModel @Inject constructor(
}.toImmutableList()
}
private fun onProviderSelected(result: SelectProviderResult, isBestRate: Boolean) {
analyticsEventHandler.send(
OnrampAnalyticsEvent.OnProviderChosen(
providerName = result.provider.info.name,
tokenSymbol = params.cryptoCurrency.symbol,
),
)
params.onProviderClick(result, isBestRate)
params.onDismiss()
}
/**
* Sorting providers by rule:
*

View file

@ -15,6 +15,7 @@ internal object SelectProviderPreviewData {
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
onClick = {},
enabled = false,
),
providers = persistentListOf(
ProviderListItemUM.Available(

View file

@ -1,12 +1,9 @@
package com.tangem.features.onramp.providers.ui
import android.content.res.Configuration
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -30,6 +27,7 @@ import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
@ -119,9 +117,19 @@ private fun ProviderItem(state: ProviderListItemUM, modifier: Modifier = Modifie
modifier = modifier,
state = state,
)
is ProviderListItemUM.AvailableWithError -> {
UnavailableProviderItem(
modifier = modifier.clickable(onClick = state.onClick),
imageUrl = state.imageUrl,
providerName = state.name,
subtitle = state.subtitle,
)
}
is ProviderListItemUM.Unavailable -> UnavailableProviderItem(
modifier = modifier,
state = state,
imageUrl = state.imageUrl,
providerName = state.name,
subtitle = state.subtitle,
)
}
}
@ -194,7 +202,12 @@ private fun AvailableProviderItem(state: ProviderListItemUM.Available, modifier:
private const val UNAVAILABLE_ALPHA = 0.4F
@Composable
private fun UnavailableProviderItem(state: ProviderListItemUM.Unavailable, modifier: Modifier = Modifier) {
private fun UnavailableProviderItem(
imageUrl: String,
providerName: String,
subtitle: TextReference,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
@ -206,7 +219,7 @@ private fun UnavailableProviderItem(state: ProviderListItemUM.Unavailable, modif
.clip(TangemTheme.shapes.roundedCorners8)
.alpha(UNAVAILABLE_ALPHA),
model = ImageRequest.Builder(context = LocalContext.current)
.data(state.imageUrl)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
@ -215,12 +228,12 @@ private fun UnavailableProviderItem(state: ProviderListItemUM.Unavailable, modif
)
Column(modifier = Modifier.weight(1F)) {
Text(
text = state.name,
text = providerName,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = state.subtitle.resolveReference(),
text = subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)