Updated on 2026-08-14

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

View file

@ -254,11 +254,13 @@ internal object OnrampDomainModule {
onrampRepository: OnrampRepository, onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver, onrampErrorResolver: OnrampErrorResolver,
onrampTransactionRepository: OnrampTransactionRepository, onrampTransactionRepository: OnrampTransactionRepository,
settingsRepository: SettingsRepository,
): GetOnrampOffersUseCase { ): GetOnrampOffersUseCase {
return GetOnrampOffersUseCase( return GetOnrampOffersUseCase(
onrampRepository = onrampRepository, onrampRepository = onrampRepository,
errorResolver = onrampErrorResolver, errorResolver = onrampErrorResolver,
onrampTransactionRepository = onrampTransactionRepository, onrampTransactionRepository = onrampTransactionRepository,
settingsRepository = settingsRepository,
) )
} }
} }

View file

@ -898,6 +898,7 @@
<string name="onramp_timing_minutes">%s min</string> <string name="onramp_timing_minutes">%s min</string>
<string name="onramp_title_you_get">You get</string> <string name="onramp_title_you_get">You get</string>
<string name="onramp_transaction_status_footer_text">You can close this screen and check the transaction status on the token details screen.</string> <string name="onramp_transaction_status_footer_text">You can close this screen and check the transaction status on the token details screen.</string>
<string name="onramp_up_to_rate">Up to</string>
<string name="onramp_via">Via</string> <string name="onramp_via">Via</string>
<string name="onramp_you_will_pay_title">You will pay</string> <string name="onramp_you_will_pay_title">You will pay</string>
<string name="organize_tokens_group">Group</string> <string name="organize_tokens_group">Group</string>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M6,0L6,0A6,6 0,0 1,12 6L12,6A6,6 0,0 1,6 12L6,12A6,6 0,0 1,0 6L0,6A6,6 0,0 1,6 0z"
android:fillColor="#0099FF"/>
<group>
<clip-path
android:pathData="M2,2h8v8h-8z"/>
<path
android:pathData="M5.703,2.789C5.829,2.55 6.172,2.55 6.298,2.789L7.152,4.407C7.201,4.5 7.29,4.564 7.393,4.582L9.196,4.894C9.462,4.94 9.568,5.266 9.38,5.46L8.104,6.773C8.032,6.848 7.998,6.952 8.013,7.055L8.273,8.867C8.312,9.135 8.034,9.336 7.791,9.217L6.149,8.409C6.055,8.363 5.946,8.363 5.852,8.409L4.21,9.217C3.967,9.336 3.689,9.135 3.728,8.867L3.988,7.055C4.003,6.952 3.969,6.848 3.897,6.773L2.621,5.46C2.433,5.266 2.538,4.94 2.805,4.894L4.608,4.582C4.711,4.564 4.8,4.5 4.849,4.407L5.703,2.789Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="10dp"
android:viewportWidth="10"
android:viewportHeight="10">
<path
android:pathData="M8.816,5.923C8.997,5.822 9.225,5.885 9.327,6.066C9.428,6.246 9.365,6.475 9.184,6.577L5.92,8.413C5.349,8.734 4.652,8.734 4.081,8.413L0.816,6.577C0.636,6.475 0.572,6.246 0.674,6.066C0.775,5.886 1.004,5.822 1.184,5.923L4.449,7.759C4.791,7.952 5.209,7.952 5.552,7.759L8.816,5.923ZM4.081,1.586C4.652,1.265 5.349,1.265 5.92,1.586L9.184,3.423C9.302,3.49 9.375,3.614 9.375,3.75C9.375,3.885 9.302,4.01 9.184,4.077L5.92,5.913C5.349,6.234 4.652,6.234 4.081,5.913L0.816,4.077C0.698,4.01 0.625,3.885 0.625,3.75C0.625,3.614 0.698,3.49 0.816,3.423L4.081,1.586ZM5.552,2.241C5.209,2.048 4.791,2.048 4.449,2.241L1.765,3.75L4.449,5.259C4.791,5.452 5.209,5.452 5.552,5.259L8.235,3.75L5.552,2.241Z"
android:fillColor="#919191"/>
</vector>

View file

@ -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.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.utils.calculateRateDif import com.tangem.domain.onramp.utils.calculateRateDif
import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -38,7 +39,7 @@ class GetOnrampAllOffersUseCase(
if (validQuotes.isEmpty()) return emptyList() if (validQuotes.isEmpty()) return emptyList()
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability() val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
val overallBestRateQuote = validQuotes.maxByOrNull { it.toAmount.value } val overallBestRateQuote = validQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable))
val bestRate = overallBestRateQuote?.toAmount?.value val bestRate = overallBestRateQuote?.toAmount?.value
val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod } val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod }
@ -54,7 +55,8 @@ class GetOnrampAllOffersUseCase(
OnrampOffer(quote = quote, rateDif = rateDif, advantages = advantages) OnrampOffer(quote = quote, rateDif = rateDif, advantages = advantages)
} }
val groupBestRateOfferData = methodQuotes.maxByOrNull { it.toAmount.value } val groupBestRateOfferData =
methodQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable))
val groupBestRateOffer = methodOffers.find { val groupBestRateOffer = methodOffers.find {
when (val quote = it.quote) { when (val quote = it.quote) {
is OnrampQuote.Data -> quote == groupBestRateOfferData is OnrampQuote.Data -> quote == groupBestRateOfferData

View file

@ -12,15 +12,17 @@ import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.onramp.utils.calculateRateDif 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.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.math.BigDecimal
class GetOnrampOffersUseCase( class GetOnrampOffersUseCase(
private val onrampRepository: OnrampRepository, private val onrampRepository: OnrampRepository,
private val onrampTransactionRepository: OnrampTransactionRepository, private val onrampTransactionRepository: OnrampTransactionRepository,
private val errorResolver: OnrampErrorResolver, private val errorResolver: OnrampErrorResolver,
private val settingsRepository: SettingsRepository,
) { ) {
operator fun invoke( operator fun invoke(
@ -37,14 +39,15 @@ class GetOnrampOffersUseCase(
.catch { throwable -> errorResolver.resolve(throwable).left() } .catch { throwable -> errorResolver.resolve(throwable).left() }
} }
private fun processOffers( private suspend fun processOffers(
quotes: List<OnrampQuote>, quotes: List<OnrampQuote>,
transactions: List<OnrampTransaction>, transactions: List<OnrampTransaction>,
): List<OnrampOffersBlock> { ): List<OnrampOffersBlock> {
val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>() val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>()
if (validQuotes.isEmpty()) return emptyList() 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 bestRate = bestRateQuote?.toAmount?.value
val offers = validQuotes.map { quote -> val offers = validQuotes.map { quote ->
@ -53,8 +56,8 @@ class GetOnrampOffersUseCase(
} }
val recentOffer = findRecentOffer(offers, transactions) val recentOffer = findRecentOffer(offers, transactions)
val bestRateOffer = findBestRateOffer(offers) val bestRateOffer = findBestRateOffer(offers, isGooglePayAvailable)
val fastestOffer = findFastestOffer(offers) val fastestOffer = findFastestOffer(offers, isGooglePayAvailable)
return buildOffersBlocks( return buildOffersBlocks(
recentOffer = recentOffer, recentOffer = recentOffer,
@ -73,36 +76,35 @@ class GetOnrampOffersUseCase(
} }
} }
private fun findBestRateOffer(offers: List<OnrampOffer>): OnrampOffer? { private fun findBestRateOffer(offers: List<OnrampOffer>, isGooglePayAvailable: Boolean): OnrampOffer? {
return offers.maxByOrNull { offer -> return offers.maxWithOrNull(offerComparator(isGooglePayAvailable))
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value
else -> BigDecimal.ZERO
}
}
} }
private fun findFastestOffer(offers: List<OnrampOffer>): OnrampOffer? { private fun findFastestOffer(offers: List<OnrampOffer>, isGooglePayAvailable: Boolean): OnrampOffer? {
val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() } val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() }
return if (instantOffers.isNotEmpty()) { return if (instantOffers.isNotEmpty()) {
instantOffers.maxByOrNull { offer -> instantOffers.maxWithOrNull(offerComparator(isGooglePayAvailable))
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value
else -> BigDecimal.ZERO
}
}
} else { } else {
val offersBySpeed = offers.groupBy { offer -> val offersBySpeed = offers.groupBy { offer ->
offer.quote.paymentMethod.type.getProcessingSpeed().speed offer.quote.paymentMethod.type.getProcessingSpeed().speed
} }
val fastestSpeed = offersBySpeed.keys.minOrNull() ?: return null val fastestSpeed = offersBySpeed.keys.minOrNull() ?: return null
val fastestOffers = offersBySpeed[fastestSpeed] ?: return null val fastestOffers = offersBySpeed[fastestSpeed] ?: return null
fastestOffers.maxByOrNull { offer -> fastestOffers.maxWithOrNull(offerComparator(isGooglePayAvailable))
when (val quote = offer.quote) { }
is OnrampQuote.Data -> quote.toAmount.value }
else -> BigDecimal.ZERO
private fun offerComparator(isGooglePayAvailable: Boolean): Comparator<OnrampOffer> = 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
} }
} }

View file

@ -1,8 +1,34 @@
package com.tangem.domain.onramp.utils package com.tangem.domain.onramp.utils
import com.tangem.domain.onramp.model.OnrampQuote
import java.math.BigDecimal import java.math.BigDecimal
internal fun calculateRateDif(currentTokenRate: BigDecimal, bestRate: BigDecimal?): BigDecimal? { internal fun calculateRateDif(currentTokenRate: BigDecimal, bestRate: BigDecimal?): BigDecimal? {
if (bestRate == null) return null if (bestRate == null) return null
return BigDecimal.ONE - currentTokenRate / bestRate return BigDecimal.ONE - currentTokenRate / bestRate
} }
internal fun compareOffersByRateSpeedAndPriority(isGooglePayAvailable: Boolean): Comparator<OnrampQuote.Data> {
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))
}
}

View file

@ -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.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import io.mockk.* import io.mockk.*
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -22,6 +23,7 @@ class GetOnrampOffersUseCaseTest {
private val onrampRepository: OnrampRepository = mockk(relaxUnitFun = true) private val onrampRepository: OnrampRepository = mockk(relaxUnitFun = true)
private val onrampTransactionRepository: OnrampTransactionRepository = mockk(relaxUnitFun = true) private val onrampTransactionRepository: OnrampTransactionRepository = mockk(relaxUnitFun = true)
private val errorResolver: OnrampErrorResolver = 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 cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true)
private val userWalletId: UserWalletId = mockk(relaxUnitFun = true) private val userWalletId: UserWalletId = mockk(relaxUnitFun = true)
@ -34,6 +36,7 @@ class GetOnrampOffersUseCaseTest {
onrampRepository = onrampRepository, onrampRepository = onrampRepository,
onrampTransactionRepository = onrampTransactionRepository, onrampTransactionRepository = onrampTransactionRepository,
errorResolver = errorResolver, errorResolver = errorResolver,
settingsRepository = settingsRepository,
) )
} }
@ -42,6 +45,7 @@ class GetOnrampOffersUseCaseTest {
val emptyQuotes = listOf<OnrampQuote>() val emptyQuotes = listOf<OnrampQuote>()
val emptyTransactions = listOf<OnrampTransaction>() val emptyTransactions = listOf<OnrampTransaction>()
coEvery { settingsRepository.isGooglePayAvailability() } returns false
coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes) coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
emptyTransactions, emptyTransactions,
@ -77,6 +81,7 @@ class GetOnrampOffersUseCaseTest {
createMockTransaction("provider1", "card", 1000L), createMockTransaction("provider1", "card", 1000L),
) )
coEvery { settingsRepository.isGooglePayAvailability() } returns false
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions, transactions,
@ -121,6 +126,7 @@ class GetOnrampOffersUseCaseTest {
val transactions = emptyList<OnrampTransaction>() val transactions = emptyList<OnrampTransaction>()
coEvery { settingsRepository.isGooglePayAvailability() } returns false
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions, transactions,
@ -165,6 +171,7 @@ class GetOnrampOffersUseCaseTest {
val transactions = emptyList<OnrampTransaction>() val transactions = emptyList<OnrampTransaction>()
coEvery { settingsRepository.isGooglePayAvailability() } returns false
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions, transactions,
@ -218,6 +225,7 @@ class GetOnrampOffersUseCaseTest {
val transactions = emptyList<OnrampTransaction>() val transactions = emptyList<OnrampTransaction>()
coEvery { settingsRepository.isGooglePayAvailability() } returns false
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf( coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions, transactions,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -118,7 +118,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
} }
override fun openSettings() { override fun openSettings() {
params.openSettings params.openSettings.invoke()
} }
override fun openCurrenciesList() { override fun openCurrenciesList() {
@ -139,7 +139,9 @@ internal class OnrampV2MainComponentModel @Inject constructor(
} }
override fun openProviders() { 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() { override fun onRefresh() {

View file

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