Updated on 2026-08-14
This commit is contained in:
parent
f39054b5a9
commit
551e2a60bb
22 changed files with 985 additions and 37 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,5 @@ internal interface OnrampMainV2ComponentModelModule {
|
|||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnrampV2MainComponentModel::class)
|
||||
fun bindOnrampSelectCountryModel(model: OnrampV2MainComponentModel): Model
|
||||
fun bindOnrampV2MainComponentModel(model: OnrampV2MainComponentModel): Model
|
||||
}
|
||||
|
|
@ -12,5 +12,5 @@ sealed interface OnrampV2MainBottomSheetConfig {
|
|||
data object CurrenciesList : OnrampV2MainBottomSheetConfig
|
||||
|
||||
@Serializable
|
||||
data object AllOffers : OnrampV2MainBottomSheetConfig
|
||||
data class AllOffers(val amountCurrencyCode: String) : OnrampV2MainBottomSheetConfig
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ internal class OnrampV2AmountFieldChangeConverter(
|
|||
continueButtonConfig = state.continueButtonConfig.copy(enabled = false),
|
||||
onrampProviderState = OnrampV2ProvidersUM.Loading,
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue