From 551e2a60bbbf61569fdf4e1ceeccfb250a3d4893 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Sep 2025 10:04:56 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/OnrampDomainModule.kt | 2 + core/res/src/main/res/values/strings.xml | 1 + .../src/main/res/drawable/ic_best_rate_12.xml | 16 + .../main/res/drawable/ic_staking_mini_10.xml | 9 + .../onramp/GetOnrampAllOffersUseCase.kt | 6 +- .../domain/onramp/GetOnrampOffersUseCase.kt | 48 +-- .../domain/onramp/utils/OnrampOfferUtils.kt | 26 ++ .../onramp/GetOnrampOffersUseCaseTest.kt | 8 + .../onramp/alloffers/AllOffersComponent.kt | 3 + .../alloffers/DefaultAllOffersComponent.kt | 9 +- .../alloffers/entity/AllOffersIntents.kt | 14 + .../alloffers/entity/AllOffersStateFactory.kt | 154 +++++++++ .../alloffers/entity/AllOffersStateUM.kt | 34 ++ .../onramp/alloffers/model/AllOffersModel.kt | 80 ++++- .../alloffers/ui/AllOffersContentSheet.kt | 322 ++++++++++++++++++ .../alloffers/ui/PaymentMethodsContent.kt | 262 ++++++++++++++ .../mainv2/DefaultOnrampV2MainComponent.kt | 4 +- .../di/OnrampMainV2ComponentModelModule.kt | 2 +- .../entity/OnrampV2MainBottomSheetConfig.kt | 2 +- .../OnrampV2AmountFieldChangeConverter.kt | 1 + .../model/OnrampV2MainComponentModel.kt | 6 +- .../onramp/mainv2/ui/OnrampOffersContent.kt | 13 +- 22 files changed, 985 insertions(+), 37 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_best_rate_12.xml create mode 100644 core/ui/src/main/res/drawable/ic_staking_mini_10.xml create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 18f3540843..a0e87e1ab1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -254,11 +254,13 @@ internal object OnrampDomainModule { onrampRepository: OnrampRepository, onrampErrorResolver: OnrampErrorResolver, onrampTransactionRepository: OnrampTransactionRepository, + settingsRepository: SettingsRepository, ): GetOnrampOffersUseCase { return GetOnrampOffersUseCase( onrampRepository = onrampRepository, errorResolver = onrampErrorResolver, onrampTransactionRepository = onrampTransactionRepository, + settingsRepository = settingsRepository, ) } } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b475fe441f..c3d240ace5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -898,6 +898,7 @@ %s min You get You can close this screen and check the transaction status on the token details screen. + Up to Via You will pay Group diff --git a/core/ui/src/main/res/drawable/ic_best_rate_12.xml b/core/ui/src/main/res/drawable/ic_best_rate_12.xml new file mode 100644 index 0000000000..99e0e500c2 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_best_rate_12.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_staking_mini_10.xml b/core/ui/src/main/res/drawable/ic_staking_mini_10.xml new file mode 100644 index 0000000000..69aab19b6c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_staking_mini_10.xml @@ -0,0 +1,9 @@ + + + diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt index f2f787e182..a7e1d2754f 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt @@ -13,6 +13,7 @@ import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.utils.calculateRateDif +import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority import com.tangem.domain.settings.repositories.SettingsRepository import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map @@ -38,7 +39,7 @@ class GetOnrampAllOffersUseCase( if (validQuotes.isEmpty()) return emptyList() val isGooglePayAvailable = settingsRepository.isGooglePayAvailability() - val overallBestRateQuote = validQuotes.maxByOrNull { it.toAmount.value } + val overallBestRateQuote = validQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable)) val bestRate = overallBestRateQuote?.toAmount?.value val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod } @@ -54,7 +55,8 @@ class GetOnrampAllOffersUseCase( OnrampOffer(quote = quote, rateDif = rateDif, advantages = advantages) } - val groupBestRateOfferData = methodQuotes.maxByOrNull { it.toAmount.value } + val groupBestRateOfferData = + methodQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable)) val groupBestRateOffer = methodOffers.find { when (val quote = it.quote) { is OnrampQuote.Data -> quote == groupBestRateOfferData diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt index 1632abd3cc..81ee5e82fd 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt @@ -12,15 +12,17 @@ import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.onramp.utils.calculateRateDif +import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority +import com.tangem.domain.settings.repositories.SettingsRepository import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map -import java.math.BigDecimal class GetOnrampOffersUseCase( private val onrampRepository: OnrampRepository, private val onrampTransactionRepository: OnrampTransactionRepository, private val errorResolver: OnrampErrorResolver, + private val settingsRepository: SettingsRepository, ) { operator fun invoke( @@ -37,14 +39,15 @@ class GetOnrampOffersUseCase( .catch { throwable -> errorResolver.resolve(throwable).left() } } - private fun processOffers( + private suspend fun processOffers( quotes: List, transactions: List, ): List { val validQuotes = quotes.filterIsInstance() if (validQuotes.isEmpty()) return emptyList() - val bestRateQuote = validQuotes.maxByOrNull { it.toAmount.value } + val isGooglePayAvailable = settingsRepository.isGooglePayAvailability() + val bestRateQuote = validQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable)) val bestRate = bestRateQuote?.toAmount?.value val offers = validQuotes.map { quote -> @@ -53,8 +56,8 @@ class GetOnrampOffersUseCase( } val recentOffer = findRecentOffer(offers, transactions) - val bestRateOffer = findBestRateOffer(offers) - val fastestOffer = findFastestOffer(offers) + val bestRateOffer = findBestRateOffer(offers, isGooglePayAvailable) + val fastestOffer = findFastestOffer(offers, isGooglePayAvailable) return buildOffersBlocks( recentOffer = recentOffer, @@ -73,36 +76,35 @@ class GetOnrampOffersUseCase( } } - private fun findBestRateOffer(offers: List): OnrampOffer? { - return offers.maxByOrNull { offer -> - when (val quote = offer.quote) { - is OnrampQuote.Data -> quote.toAmount.value - else -> BigDecimal.ZERO - } - } + private fun findBestRateOffer(offers: List, isGooglePayAvailable: Boolean): OnrampOffer? { + return offers.maxWithOrNull(offerComparator(isGooglePayAvailable)) } - private fun findFastestOffer(offers: List): OnrampOffer? { + private fun findFastestOffer(offers: List, isGooglePayAvailable: Boolean): OnrampOffer? { val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() } return if (instantOffers.isNotEmpty()) { - instantOffers.maxByOrNull { offer -> - when (val quote = offer.quote) { - is OnrampQuote.Data -> quote.toAmount.value - else -> BigDecimal.ZERO - } - } + instantOffers.maxWithOrNull(offerComparator(isGooglePayAvailable)) } else { val offersBySpeed = offers.groupBy { offer -> offer.quote.paymentMethod.type.getProcessingSpeed().speed } val fastestSpeed = offersBySpeed.keys.minOrNull() ?: return null val fastestOffers = offersBySpeed[fastestSpeed] ?: return null - fastestOffers.maxByOrNull { offer -> - when (val quote = offer.quote) { - is OnrampQuote.Data -> quote.toAmount.value - else -> BigDecimal.ZERO + fastestOffers.maxWithOrNull(offerComparator(isGooglePayAvailable)) + } + } + + private fun offerComparator(isGooglePayAvailable: Boolean): Comparator = Comparator { offer1, offer2 -> + when (val quote1 = offer1.quote) { + is OnrampQuote.Data -> { + when (val quote2 = offer2.quote) { + is OnrampQuote.Data -> { + compareOffersByRateSpeedAndPriority(isGooglePayAvailable).compare(quote1, quote2) + } + else -> 1 } } + else -> -1 } } diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt index 0b3625d494..7329a88a84 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt @@ -1,8 +1,34 @@ package com.tangem.domain.onramp.utils +import com.tangem.domain.onramp.model.OnrampQuote import java.math.BigDecimal internal fun calculateRateDif(currentTokenRate: BigDecimal, bestRate: BigDecimal?): BigDecimal? { if (bestRate == null) return null return BigDecimal.ONE - currentTokenRate / bestRate +} + +internal fun compareOffersByRateSpeedAndPriority(isGooglePayAvailable: Boolean): Comparator { + return Comparator { quote1, quote2 -> + val rateComparison = quote1 + .toAmount + .value + .compareTo(quote2.toAmount.value) + if (rateComparison != 0) return@Comparator rateComparison + + val speedComparison = + quote2 + .paymentMethod + .type + .getProcessingSpeed() + .speed + .compareTo(quote1.paymentMethod.type.getProcessingSpeed().speed) + if (speedComparison != 0) return@Comparator speedComparison + + quote1 + .paymentMethod + .type + .getPriority(isGooglePayAvailable) + .compareTo(quote2.paymentMethod.type.getPriority(isGooglePayAvailable)) + } } \ No newline at end of file diff --git a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt index 62b55b230a..87f9fbb1c6 100644 --- a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt +++ b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt @@ -8,6 +8,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.settings.repositories.SettingsRepository import io.mockk.* import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -22,6 +23,7 @@ class GetOnrampOffersUseCaseTest { private val onrampRepository: OnrampRepository = mockk(relaxUnitFun = true) private val onrampTransactionRepository: OnrampTransactionRepository = mockk(relaxUnitFun = true) private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true) + private val settingsRepository: SettingsRepository = mockk(relaxUnitFun = true) private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true) private val userWalletId: UserWalletId = mockk(relaxUnitFun = true) @@ -34,6 +36,7 @@ class GetOnrampOffersUseCaseTest { onrampRepository = onrampRepository, onrampTransactionRepository = onrampTransactionRepository, errorResolver = errorResolver, + settingsRepository = settingsRepository, ) } @@ -42,6 +45,7 @@ class GetOnrampOffersUseCaseTest { val emptyQuotes = listOf() val emptyTransactions = listOf() + coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes) coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( emptyTransactions, @@ -77,6 +81,7 @@ class GetOnrampOffersUseCaseTest { createMockTransaction("provider1", "card", 1000L), ) + coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( transactions, @@ -121,6 +126,7 @@ class GetOnrampOffersUseCaseTest { val transactions = emptyList() + coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( transactions, @@ -165,6 +171,7 @@ class GetOnrampOffersUseCaseTest { val transactions = emptyList() + coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( transactions, @@ -218,6 +225,7 @@ class GetOnrampOffersUseCaseTest { val transactions = emptyList() + coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( transactions, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt index 7c2de6da52..d2d2615c0e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/AllOffersComponent.kt @@ -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 diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt index 02adaaa504..12c4ec31f9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/DefaultAllOffersComponent.kt @@ -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 diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt new file mode 100644 index 0000000000..4d2bbcec90 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt @@ -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() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt new file mode 100644 index 0000000000..67878d0738 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -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, + private val allOffersIntents: AllOffersIntents, +) { + + fun getLoadedPaymentsState(methodGroups: List): 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): List { + 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 + } + } + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt new file mode 100644 index 0000000000..834bc396b3 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt @@ -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, + val currentMethod: AllOffersPaymentMethodUM? = null, + val onBackClicked: () -> Unit, + ) : AllOffersStateUM + + data class Error(val errorNotification: NotificationUM) : AllOffersStateUM +} + +internal data class AllOffersPaymentMethodUM( + val offers: ImmutableList, + 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, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index 3280c17cb9..19e69d6696 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -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 = MutableStateFlow(AllOffersStateUM.Loading) + val state: StateFlow = _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) } + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt new file mode 100644 index 0000000000..f60660546a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt @@ -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( + 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) { + 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 = {}, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt new file mode 100644 index 0000000000..49d457c4b2 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt @@ -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) { + 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)) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt index 2487bf37b1..af62d7ecbe 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt @@ -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, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt index 6d20f87cf7..84fb039ffd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt @@ -16,5 +16,5 @@ internal interface OnrampMainV2ComponentModelModule { @Binds @IntoMap @ClassKey(OnrampV2MainComponentModel::class) - fun bindOnrampSelectCountryModel(model: OnrampV2MainComponentModel): Model + fun bindOnrampV2MainComponentModel(model: OnrampV2MainComponentModel): Model } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt index d9e6c47a2d..afc654a422 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt @@ -12,5 +12,5 @@ sealed interface OnrampV2MainBottomSheetConfig { data object CurrenciesList : OnrampV2MainBottomSheetConfig @Serializable - data object AllOffers : OnrampV2MainBottomSheetConfig + data class AllOffers(val amountCurrencyCode: String) : OnrampV2MainBottomSheetConfig } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt index 2f8f0f4186..e0947e34b5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt @@ -45,6 +45,7 @@ internal class OnrampV2AmountFieldChangeConverter( continueButtonConfig = state.continueButtonConfig.copy(enabled = false), onrampProviderState = OnrampV2ProvidersUM.Loading, onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + offersBlockState = OnrampOffersBlockUM.Empty, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt index d83a94d1f0..abead667d7 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt @@ -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() { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt index 1fc602096c..05645f62d0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt @@ -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)