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)