Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-09 10:04:56 +02:00
parent f39054b5a9
commit 551e2a60bb
22 changed files with 985 additions and 37 deletions

View file

@ -4,6 +4,7 @@ import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
internal interface AllOffersComponent : ComposableBottomSheetComponent {
@ -11,6 +12,8 @@ internal interface AllOffersComponent : ComposableBottomSheetComponent {
val userWallet: UserWallet,
val cryptoCurrency: CryptoCurrency,
val onDismiss: () -> Unit,
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
val amountCurrencyCode: String,
)
interface Factory : ComponentFactory<Params, AllOffersComponent>

View file

@ -1,9 +1,12 @@
package com.tangem.features.onramp.alloffers
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.alloffers.model.AllOffersModel
import com.tangem.features.onramp.alloffers.ui.AllOffersContentSheet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -21,7 +24,11 @@ internal class DefaultAllOffersComponent @AssistedInject constructor(
@Composable
override fun BottomSheet() {
// TODO to be continued in [REDACTED_TASK_KEY]
val state by model.state.collectAsState()
AllOffersContentSheet(
state = state,
onCloseClick = { dismiss() },
)
}
@AssistedFactory

View file

@ -0,0 +1,14 @@
package com.tangem.features.onramp.alloffers.entity
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
interface AllOffersIntents {
fun onPaymentMethodClicked(paymentMethodId: String)
fun onBuyClick(quote: OnrampProviderWithQuote.Data)
fun onBackClicked()
fun onRefresh()
}

View file

@ -0,0 +1,154 @@
package com.tangem.features.onramp.alloffers.entity
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.onramp.model.*
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.MINUS
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal class AllOffersStateFactory(
private val currentStateProvider: Provider<AllOffersStateUM>,
private val allOffersIntents: AllOffersIntents,
) {
fun getLoadedPaymentsState(methodGroups: List<OnrampPaymentMethodGroup>): AllOffersStateUM {
return AllOffersStateUM.Content(
methods = methodGroups.map { methodGroup ->
AllOffersPaymentMethodUM(
offers = mapOffersToUM(methodGroup.offers).toPersistentList(),
methodConfig = OnrampPaymentMethodConfig(
method = methodGroup.paymentMethod,
onClick = { allOffersIntents.onPaymentMethodClicked(methodGroup.paymentMethod.id) },
),
diff = methodGroup
.bestRateOffer
?.rateDif
?.takeIf { it > BigDecimal.ZERO }
?.let { diff ->
stringReference("$MINUS${diff.format { percent() }}")
},
rate = methodGroup.bestRateOffer?.let { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value.format {
crypto(
symbol = quote.toAmount.symbol,
decimals = quote.toAmount.decimals,
)
}
else -> ""
}
} ?: "",
providersCount = methodGroup.providerCount,
isBestRate = methodGroup.isBestPaymentMethod,
)
}.toPersistentList(),
currentMethod = null,
onBackClicked = { allOffersIntents.onBackClicked() },
)
}
fun getPaymentsState(): AllOffersStateUM {
return when (val currentState = currentStateProvider.invoke()) {
is AllOffersStateUM.Content -> {
currentState.copy(currentMethod = null)
}
AllOffersStateUM.Loading,
is AllOffersStateUM.Error,
-> currentState
}
}
fun getOnrampErrorState(onrampError: OnrampError): AllOffersStateUM {
return when (onrampError) {
is OnrampError.DataError -> getErrorState(
errorCode = onrampError.code,
onRefresh = allOffersIntents::onRefresh,
)
OnrampError.PairsNotFound,
is OnrampError.DomainError,
-> getErrorState(onRefresh = allOffersIntents::onRefresh)
is OnrampError.AmountError.TooBigError,
is OnrampError.AmountError.TooSmallError,
OnrampError.RedirectError.VerificationFailed,
OnrampError.RedirectError.WrongRequestId,
-> currentStateProvider()
}
}
private fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): AllOffersStateUM {
val state = currentStateProvider()
return when (state) {
is AllOffersStateUM.Content,
AllOffersStateUM.Loading,
-> AllOffersStateUM.Error(
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
errorCode = errorCode,
onRefresh = onRefresh,
),
)
is AllOffersStateUM.Error -> state
}
}
private fun mapOfferAdvantagesDTOtoUM(advantages: OnrampOfferAdvantages): OnrampOfferAdvantagesUM {
return when (advantages) {
OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default
OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate
OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest
}
}
private fun mapOffersToUM(offers: List<OnrampOffer>): List<OnrampOfferUM> {
return buildList {
offers.forEach { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> {
add(
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = mapOfferAdvantagesDTOtoUM(offer.advantages),
paymentMethod = quote.paymentMethod,
providerId = quote.provider.id,
providerName = quote.provider.info.name,
rate = quote.toAmount.value.format {
crypto(
symbol = quote.toAmount.symbol,
decimals = quote.toAmount.decimals,
)
},
diff = offer
.rateDif
?.takeIf { it > BigDecimal.ZERO }
?.let { diff ->
stringReference("$MINUS${diff.format { percent() }}")
},
onBuyClicked = {
allOffersIntents.onBuyClick(
OnrampProviderWithQuote.Data(
provider = quote.provider,
paymentMethod = quote.paymentMethod,
toAmount = quote.toAmount,
fromAmount = quote.fromAmount,
),
)
},
),
)
}
is OnrampQuote.AmountError,
is OnrampQuote.Error,
-> Unit
}
}
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.onramp.alloffers.entity
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
import kotlinx.collections.immutable.ImmutableList
internal sealed interface AllOffersStateUM {
data object Loading : AllOffersStateUM
data class Content(
val methods: ImmutableList<AllOffersPaymentMethodUM>,
val currentMethod: AllOffersPaymentMethodUM? = null,
val onBackClicked: () -> Unit,
) : AllOffersStateUM
data class Error(val errorNotification: NotificationUM) : AllOffersStateUM
}
internal data class AllOffersPaymentMethodUM(
val offers: ImmutableList<OnrampOfferUM>,
val methodConfig: OnrampPaymentMethodConfig,
val diff: TextReference?,
val rate: String,
val providersCount: Int,
val isBestRate: Boolean,
)
internal data class OnrampPaymentMethodConfig(
val method: OnrampPaymentMethod,
val onClick: () -> Unit,
)

View file

@ -1,19 +1,97 @@
package com.tangem.features.onramp.alloffers.model
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.onramp.GetOnrampAllOffersUseCase
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.alloffers.AllOffersComponent
import com.tangem.features.onramp.alloffers.entity.AllOffersIntents
import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
internal class AllOffersModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getOnrampAllOffersUseCase: GetOnrampAllOffersUseCase,
paramsContainer: ParamsContainer,
) : Model() {
) : Model(), AllOffersIntents {
private var quotesJob: Job? = null
private val stateFactory: AllOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
AllOffersStateFactory(
currentStateProvider = Provider { state.value },
allOffersIntents = this,
)
}
private val params: AllOffersComponent.Params = paramsContainer.require()
private val _state: MutableStateFlow<AllOffersStateUM> = MutableStateFlow(AllOffersStateUM.Loading)
val state: StateFlow<AllOffersStateUM> = _state.asStateFlow()
init {
subscribeOnAllOffers()
}
fun dismiss() {
params.onDismiss()
}
override fun onPaymentMethodClicked(paymentMethodId: String) {
val contentState = state.value as? AllOffersStateUM.Content ?: return
val method = contentState.methods.firstOrNull { it.methodConfig.method.id == paymentMethodId } ?: return
_state.update { contentState.copy(currentMethod = method) }
}
override fun onBuyClick(quote: OnrampProviderWithQuote.Data) {
analyticsEventHandler.send(
OnrampAnalyticsEvent.OnBuyClick(
providerName = quote.provider.info.name,
currency = params.amountCurrencyCode,
tokenSymbol = params.cryptoCurrency.symbol,
),
)
params.openRedirectPage(quote)
}
override fun onBackClicked() {
_state.update { stateFactory.getPaymentsState() }
}
override fun onRefresh() {
subscribeOnAllOffers()
}
private fun subscribeOnAllOffers() {
quotesJob?.cancel()
quotesJob = modelScope.launch(dispatchers.default) {
getOnrampAllOffersUseCase.invoke(
userWalletId = params.userWallet.walletId,
cryptoCurrencyId = params.cryptoCurrency.id,
).collectLatest { maybeOffers ->
maybeOffers.fold(
ifLeft = ::handleOnrampError,
ifRight = { offersGroup ->
_state.update { stateFactory.getLoadedPaymentsState(offersGroup) }
},
)
}
}
}
private fun handleOnrampError(onrampError: OnrampError) {
Timber.e(onrampError.toString())
_state.update { stateFactory.getOnrampErrorState(onrampError) }
}
}

View file

@ -0,0 +1,322 @@
package com.tangem.features.onramp.alloffers.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
import com.tangem.features.onramp.mainv2.ui.Offer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@Composable
internal fun AllOffersContentSheet(state: AllOffersStateUM, onCloseClick: () -> Unit) {
val onBack = remember(state) {
{
if (state is AllOffersStateUM.Content && state.currentMethod != null) {
state.onBackClicked()
} else {
onCloseClick()
}
}
}
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onCloseClick,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = onBack,
containerColor = TangemTheme.colors.background.tertiary,
title = {
if (state is AllOffersStateUM.Content && state.currentMethod != null) {
ProviderTitle(
onCloseClick = onCloseClick,
onBackClick = onBack,
)
} else {
PaymentMethodTitle(onCloseClick = onCloseClick)
}
},
content = {
Box(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
.animateContentSize(),
) {
AnimatedContent(
targetState = state is AllOffersStateUM.Content && state.currentMethod != null,
transitionSpec = {
fadeIn(tween(durationMillis = 220)) togetherWith
fadeOut(tween(durationMillis = 220))
},
label = "Change offers and payment method state",
) { shouldShowOffersScreen ->
when (state) {
AllOffersStateUM.Loading -> AllOffersContentLoading()
is AllOffersStateUM.Error -> AllOffersError(state.errorNotification)
is AllOffersStateUM.Content -> {
if (shouldShowOffersScreen) {
state.currentMethod?.let {
OffersBasedOnPaymentMethodContent(offers = it.offers)
}
} else {
PaymentMethodsContent(methods = state.methods)
}
}
}
}
}
},
)
}
@Composable
private fun ProviderTitle(onBackClick: () -> Unit, onCloseClick: () -> Unit) {
TangemModalBottomSheetTitle(
title = TextReference.Res(R.string.onramp_all_offers_button_title),
subtitle = TextReference.Res(R.string.express_choose_providers_title),
startIconRes = R.drawable.ic_back_24,
onStartClick = onBackClick,
endIconRes = com.tangem.core.ui.R.drawable.ic_close_24,
onEndClick = onCloseClick,
)
}
@Composable
private fun PaymentMethodTitle(onCloseClick: () -> Unit) {
TangemModalBottomSheetTitle(
title = TextReference.Res(R.string.onramp_all_offers_button_title),
subtitle = TextReference.Res(R.string.onramp_payment_method_subtitle),
endIconRes = com.tangem.core.ui.R.drawable.ic_close_24,
onEndClick = onCloseClick,
)
}
@Composable
private fun OffersBasedOnPaymentMethodContent(offers: ImmutableList<OnrampOfferUM>) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
offers.fastForEach { offer ->
key("${offer.paymentMethod.id} ${offer.providerName} ${offer.rate}") {
Offer(offer)
SpacerH(8.dp)
}
}
}
}
@Composable
private fun AllOffersContentLoading() {
Column(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors.background.tertiary)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
RectangleShimmer(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
.height(96.dp)
.size(width = 76.dp, height = 20.dp),
radius = 14.dp,
)
RectangleShimmer(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
.height(96.dp)
.size(width = 76.dp, height = 20.dp),
radius = 14.dp,
)
RectangleShimmer(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
.height(96.dp)
.size(width = 76.dp, height = 20.dp),
radius = 14.dp,
)
}
}
@Composable
fun AllOffersError(errorNotification: NotificationUM) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
Notification(config = errorNotification.config)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AllOffersContentSheetPaymentPreview() {
val method = AllOffersPaymentMethodUM(
offers = persistentListOf(
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.BestRate,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId1",
providerName = "Simplex",
rate = "0,0245334 BTC",
diff = null,
onBuyClicked = {},
),
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.Default,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId2",
providerName = "Simplex",
rate = "0,00145334 BTC",
diff = stringReference("0.07%"),
onBuyClicked = {},
),
),
methodConfig = OnrampPaymentMethodConfig(
method = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
onClick = {},
),
diff = null,
rate = "0,0245334 BTC",
providersCount = 2,
isBestRate = true,
)
TangemThemePreview {
AllOffersContentSheet(
state = AllOffersStateUM.Content(
methods = persistentListOf(),
currentMethod = method,
onBackClicked = {},
),
onCloseClick = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AllOffersContentSheetOffersPreview() {
val methods = List(5) {
AllOffersPaymentMethodUM(
offers = persistentListOf(
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.BestRate,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl =
"https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId1",
providerName = "Simplex",
rate = "0,0245334 BTC",
diff = null,
onBuyClicked = {},
),
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.Default,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl =
"https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId2",
providerName = "Simplex",
rate = "0,00145334 BTC",
diff = stringReference("0.07%"),
onBuyClicked = {},
),
),
methodConfig = OnrampPaymentMethodConfig(
method = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
onClick = {},
),
diff = null,
rate = "0,0245334 BTC",
providersCount = 2,
isBestRate = true,
)
}
TangemThemePreview {
AllOffersContentSheet(
state = AllOffersStateUM.Content(
methods = methods.toPersistentList(),
currentMethod = null,
onBackClicked = {},
),
onCloseClick = {},
)
}
}

View file

@ -0,0 +1,262 @@
package com.tangem.features.onramp.alloffers.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
import com.tangem.features.onramp.mainv2.ui.TimingBlock
import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PaymentMethodsContent(methods: ImmutableList<AllOffersPaymentMethodUM>) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
methods.fastForEach { method ->
key(method.methodConfig.method.id) {
PaymentMethod(methodUM = method)
SpacerH(8.dp)
}
}
}
}
@Composable
private fun PaymentMethod(methodUM: AllOffersPaymentMethodUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.action,
shape = RoundedCornerShape(14.dp),
)
.clickable(
indication = ripple(),
interactionSource = remember { MutableInteractionSource() },
onClick = methodUM.methodConfig.onClick,
)
.padding(
start = 12.dp,
end = 12.dp,
top = 14.dp,
bottom = 12.dp,
),
) {
PaymentMethodIcon(
modifier = Modifier.size(36.dp),
imageUrl = methodUM.methodConfig.method.imageUrl,
)
SpacerW(12.dp)
Column {
PaymentMethodInfoBlock(
paymentMethodName = methodUM.methodConfig.method.name,
rate = methodUM.rate,
diff = methodUM.diff,
isBestRate = methodUM.isBestRate,
)
Row(verticalAlignment = Alignment.CenterVertically) {
ProvidersCountBlockInfo(providersCount = methodUM.providersCount)
SpacerW(8.dp)
TimingBlockInfo(speed = methodUM.methodConfig.method.type.getProcessingSpeed())
}
}
}
}
@Composable
private fun PaymentMethodInfoBlock(
paymentMethodName: String,
rate: String,
diff: TextReference?,
isBestRate: Boolean,
) {
Column(modifier = Modifier.padding(bottom = 14.dp)) {
Text(
text = paymentMethodName,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
SpacerH(2.dp)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = stringResourceSafe(R.string.onramp_up_to_rate),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
Text(
text = rate,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
when {
isBestRate -> {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_best_rate_12),
contentDescription = null,
)
}
diff != null -> {
Text(
modifier = Modifier
.background(
color = TangemTheme.colors.text.warning.copy(alpha = 0.1f),
shape = RoundedCornerShape(4.dp),
)
.padding(horizontal = 4.dp),
text = diff.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.warning,
)
}
}
}
}
}
@Composable
private fun ProvidersCountBlockInfo(providersCount: Int) {
BorderedRow {
Icon(
modifier = Modifier.size(10.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerW(4.dp)
Text(
text = pluralStringResourceSafe(
id = R.plurals.onramp_providers_count,
count = providersCount,
providersCount,
),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun TimingBlockInfo(speed: PaymentMethodType.PaymentSpeed) {
BorderedRow {
Icon(
modifier = Modifier.size(10.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_staking_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerW(4.dp)
TimingBlock(speed)
}
}
@Composable
fun BorderedRow(content: @Composable RowScope.() -> Unit) {
Row(
modifier = Modifier
.border(
width = 1.dp,
color = TangemTheme.colors.stroke.primary,
shape = RoundedCornerShape(6.dp),
)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
content()
}
}
@Preview
@Composable
private fun PaymentMethodsContentPreview() {
val method = AllOffersPaymentMethodUM(
offers = persistentListOf(
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.BestRate,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId1",
providerName = "Simplex",
rate = "0,0245334 BTC",
diff = null,
onBuyClicked = {},
),
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.Default,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId2",
providerName = "Simplex",
rate = "0,00145334 BTC",
diff = stringReference("0.07%"),
onBuyClicked = {},
),
),
methodConfig = OnrampPaymentMethodConfig(
method = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
onClick = {},
),
diff = null,
rate = "0,0245334 BTC",
providersCount = 2,
isBestRate = true,
)
TangemThemePreview {
PaymentMethodsContent(persistentListOf(method))
}
}

View file

@ -69,12 +69,14 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create(
is OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create(
context = childByContext(componentContext),
params = AllOffersComponent.Params(
userWallet = model.userWallet,
cryptoCurrency = params.cryptoCurrency,
onDismiss = model.bottomSheetNavigation::dismiss,
openRedirectPage = params.openRedirectPage,
amountCurrencyCode = config.amountCurrencyCode,
),
)
}

View file

@ -16,5 +16,5 @@ internal interface OnrampMainV2ComponentModelModule {
@Binds
@IntoMap
@ClassKey(OnrampV2MainComponentModel::class)
fun bindOnrampSelectCountryModel(model: OnrampV2MainComponentModel): Model
fun bindOnrampV2MainComponentModel(model: OnrampV2MainComponentModel): Model
}

View file

@ -12,5 +12,5 @@ sealed interface OnrampV2MainBottomSheetConfig {
data object CurrenciesList : OnrampV2MainBottomSheetConfig
@Serializable
data object AllOffers : OnrampV2MainBottomSheetConfig
data class AllOffers(val amountCurrencyCode: String) : OnrampV2MainBottomSheetConfig
}

View file

@ -45,6 +45,7 @@ internal class OnrampV2AmountFieldChangeConverter(
continueButtonConfig = state.continueButtonConfig.copy(enabled = false),
onrampProviderState = OnrampV2ProvidersUM.Loading,
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
offersBlockState = OnrampOffersBlockUM.Empty,
)
}

View file

@ -118,7 +118,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
}
override fun openSettings() {
params.openSettings
params.openSettings.invoke()
}
override fun openCurrenciesList() {
@ -139,7 +139,9 @@ internal class OnrampV2MainComponentModel @Inject constructor(
}
override fun openProviders() {
bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers)
val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return
val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code
bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers(amountCurrentCode))
}
override fun onRefresh() {

View file

@ -12,6 +12,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
@ -80,9 +81,11 @@ internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
SpacerH(8.dp)
state.recommended.fastForEach {
Offer(it)
SpacerH(8.dp)
state.recommended.fastForEach { offer ->
key("${offer.paymentMethod.id} ${offer.providerName} ${offer.rate}") {
Offer(offer)
SpacerH(8.dp)
}
}
state.onrampAllOffersButtonConfig?.let {
@ -99,7 +102,7 @@ internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
}
@Composable
private fun Offer(onrampOfferUM: OnrampOfferUM, modifier: Modifier = Modifier) {
internal fun Offer(onrampOfferUM: OnrampOfferUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
@ -263,7 +266,7 @@ private fun PaymentBlockInOffer(paymentMethod: OnrampPaymentMethod, providerName
}
@Composable
private fun TimingBlock(speed: PaymentMethodType.PaymentSpeed) {
internal fun TimingBlock(speed: PaymentMethodType.PaymentSpeed) {
val timingText = when (speed) {
PaymentMethodType.PaymentSpeed.Instant -> {
stringResourceSafe(id = R.string.onramp_instant_status)