Updated on 2026-08-14
This commit is contained in:
parent
f39054b5a9
commit
551e2a60bb
22 changed files with 985 additions and 37 deletions
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
16
core/ui/src/main/res/drawable/ic_best_rate_12.xml
Normal file
16
core/ui/src/main/res/drawable/ic_best_rate_12.xml
Normal 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>
|
||||||
9
core/ui/src/main/res/drawable/ic_staking_mini_10.xml
Normal file
9
core/ui/src/main/res/drawable/ic_staking_mini_10.xml
Normal 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>
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.tangem.features.onramp.alloffers.entity
|
||||||
|
|
||||||
|
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||||
|
|
||||||
|
interface AllOffersIntents {
|
||||||
|
|
||||||
|
fun onPaymentMethodClicked(paymentMethodId: String)
|
||||||
|
|
||||||
|
fun onBuyClick(quote: OnrampProviderWithQuote.Data)
|
||||||
|
|
||||||
|
fun onBackClicked()
|
||||||
|
|
||||||
|
fun onRefresh()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
package com.tangem.features.onramp.alloffers.entity
|
||||||
|
|
||||||
|
import com.tangem.common.ui.notifications.NotificationUM
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.percent
|
||||||
|
import com.tangem.domain.onramp.model.*
|
||||||
|
import com.tangem.domain.onramp.model.error.OnrampError
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||||
|
import com.tangem.utils.Provider
|
||||||
|
import com.tangem.utils.StringsSigns.MINUS
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
internal class AllOffersStateFactory(
|
||||||
|
private val currentStateProvider: Provider<AllOffersStateUM>,
|
||||||
|
private val allOffersIntents: AllOffersIntents,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun getLoadedPaymentsState(methodGroups: List<OnrampPaymentMethodGroup>): AllOffersStateUM {
|
||||||
|
return AllOffersStateUM.Content(
|
||||||
|
methods = methodGroups.map { methodGroup ->
|
||||||
|
AllOffersPaymentMethodUM(
|
||||||
|
offers = mapOffersToUM(methodGroup.offers).toPersistentList(),
|
||||||
|
methodConfig = OnrampPaymentMethodConfig(
|
||||||
|
method = methodGroup.paymentMethod,
|
||||||
|
onClick = { allOffersIntents.onPaymentMethodClicked(methodGroup.paymentMethod.id) },
|
||||||
|
),
|
||||||
|
diff = methodGroup
|
||||||
|
.bestRateOffer
|
||||||
|
?.rateDif
|
||||||
|
?.takeIf { it > BigDecimal.ZERO }
|
||||||
|
?.let { diff ->
|
||||||
|
stringReference("$MINUS${diff.format { percent() }}")
|
||||||
|
},
|
||||||
|
rate = methodGroup.bestRateOffer?.let { offer ->
|
||||||
|
when (val quote = offer.quote) {
|
||||||
|
is OnrampQuote.Data -> quote.toAmount.value.format {
|
||||||
|
crypto(
|
||||||
|
symbol = quote.toAmount.symbol,
|
||||||
|
decimals = quote.toAmount.decimals,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
} ?: "",
|
||||||
|
providersCount = methodGroup.providerCount,
|
||||||
|
isBestRate = methodGroup.isBestPaymentMethod,
|
||||||
|
)
|
||||||
|
}.toPersistentList(),
|
||||||
|
currentMethod = null,
|
||||||
|
onBackClicked = { allOffersIntents.onBackClicked() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPaymentsState(): AllOffersStateUM {
|
||||||
|
return when (val currentState = currentStateProvider.invoke()) {
|
||||||
|
is AllOffersStateUM.Content -> {
|
||||||
|
currentState.copy(currentMethod = null)
|
||||||
|
}
|
||||||
|
AllOffersStateUM.Loading,
|
||||||
|
is AllOffersStateUM.Error,
|
||||||
|
-> currentState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOnrampErrorState(onrampError: OnrampError): AllOffersStateUM {
|
||||||
|
return when (onrampError) {
|
||||||
|
is OnrampError.DataError -> getErrorState(
|
||||||
|
errorCode = onrampError.code,
|
||||||
|
onRefresh = allOffersIntents::onRefresh,
|
||||||
|
)
|
||||||
|
OnrampError.PairsNotFound,
|
||||||
|
is OnrampError.DomainError,
|
||||||
|
-> getErrorState(onRefresh = allOffersIntents::onRefresh)
|
||||||
|
is OnrampError.AmountError.TooBigError,
|
||||||
|
is OnrampError.AmountError.TooSmallError,
|
||||||
|
OnrampError.RedirectError.VerificationFailed,
|
||||||
|
OnrampError.RedirectError.WrongRequestId,
|
||||||
|
-> currentStateProvider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): AllOffersStateUM {
|
||||||
|
val state = currentStateProvider()
|
||||||
|
return when (state) {
|
||||||
|
is AllOffersStateUM.Content,
|
||||||
|
AllOffersStateUM.Loading,
|
||||||
|
-> AllOffersStateUM.Error(
|
||||||
|
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||||
|
errorCode = errorCode,
|
||||||
|
onRefresh = onRefresh,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
is AllOffersStateUM.Error -> state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapOfferAdvantagesDTOtoUM(advantages: OnrampOfferAdvantages): OnrampOfferAdvantagesUM {
|
||||||
|
return when (advantages) {
|
||||||
|
OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default
|
||||||
|
OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate
|
||||||
|
OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapOffersToUM(offers: List<OnrampOffer>): List<OnrampOfferUM> {
|
||||||
|
return buildList {
|
||||||
|
offers.forEach { offer ->
|
||||||
|
when (val quote = offer.quote) {
|
||||||
|
is OnrampQuote.Data -> {
|
||||||
|
add(
|
||||||
|
OnrampOfferUM(
|
||||||
|
category = OnrampOfferCategoryUM.Recommended,
|
||||||
|
advantages = mapOfferAdvantagesDTOtoUM(offer.advantages),
|
||||||
|
paymentMethod = quote.paymentMethod,
|
||||||
|
providerId = quote.provider.id,
|
||||||
|
providerName = quote.provider.info.name,
|
||||||
|
rate = quote.toAmount.value.format {
|
||||||
|
crypto(
|
||||||
|
symbol = quote.toAmount.symbol,
|
||||||
|
decimals = quote.toAmount.decimals,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
diff = offer
|
||||||
|
.rateDif
|
||||||
|
?.takeIf { it > BigDecimal.ZERO }
|
||||||
|
?.let { diff ->
|
||||||
|
stringReference("$MINUS${diff.format { percent() }}")
|
||||||
|
},
|
||||||
|
onBuyClicked = {
|
||||||
|
allOffersIntents.onBuyClick(
|
||||||
|
OnrampProviderWithQuote.Data(
|
||||||
|
provider = quote.provider,
|
||||||
|
paymentMethod = quote.paymentMethod,
|
||||||
|
toAmount = quote.toAmount,
|
||||||
|
fromAmount = quote.fromAmount,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is OnrampQuote.AmountError,
|
||||||
|
is OnrampQuote.Error,
|
||||||
|
-> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.tangem.features.onramp.alloffers.entity
|
||||||
|
|
||||||
|
import com.tangem.common.ui.notifications.NotificationUM
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
internal sealed interface AllOffersStateUM {
|
||||||
|
|
||||||
|
data object Loading : AllOffersStateUM
|
||||||
|
|
||||||
|
data class Content(
|
||||||
|
val methods: ImmutableList<AllOffersPaymentMethodUM>,
|
||||||
|
val currentMethod: AllOffersPaymentMethodUM? = null,
|
||||||
|
val onBackClicked: () -> Unit,
|
||||||
|
) : AllOffersStateUM
|
||||||
|
|
||||||
|
data class Error(val errorNotification: NotificationUM) : AllOffersStateUM
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class AllOffersPaymentMethodUM(
|
||||||
|
val offers: ImmutableList<OnrampOfferUM>,
|
||||||
|
val methodConfig: OnrampPaymentMethodConfig,
|
||||||
|
val diff: TextReference?,
|
||||||
|
val rate: String,
|
||||||
|
val providersCount: Int,
|
||||||
|
val isBestRate: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class OnrampPaymentMethodConfig(
|
||||||
|
val method: OnrampPaymentMethod,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -1,19 +1,97 @@
|
||||||
package com.tangem.features.onramp.alloffers.model
|
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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,322 @@
|
||||||
|
package com.tangem.features.onramp.alloffers.ui
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import com.tangem.common.ui.notifications.NotificationUM
|
||||||
|
import com.tangem.core.ui.components.RectangleShimmer
|
||||||
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||||
|
import com.tangem.core.ui.components.notifications.Notification
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||||
|
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||||
|
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
||||||
|
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
|
||||||
|
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
|
||||||
|
import com.tangem.features.onramp.impl.R
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||||
|
import com.tangem.features.onramp.mainv2.ui.Offer
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun AllOffersContentSheet(state: AllOffersStateUM, onCloseClick: () -> Unit) {
|
||||||
|
val onBack = remember(state) {
|
||||||
|
{
|
||||||
|
if (state is AllOffersStateUM.Content && state.currentMethod != null) {
|
||||||
|
state.onBackClicked()
|
||||||
|
} else {
|
||||||
|
onCloseClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||||
|
config = TangemBottomSheetConfig(
|
||||||
|
isShown = true,
|
||||||
|
onDismissRequest = onCloseClick,
|
||||||
|
content = TangemBottomSheetConfigContent.Empty,
|
||||||
|
),
|
||||||
|
onBack = onBack,
|
||||||
|
containerColor = TangemTheme.colors.background.tertiary,
|
||||||
|
title = {
|
||||||
|
if (state is AllOffersStateUM.Content && state.currentMethod != null) {
|
||||||
|
ProviderTitle(
|
||||||
|
onCloseClick = onCloseClick,
|
||||||
|
onBackClick = onBack,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PaymentMethodTitle(onCloseClick = onCloseClick)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.animateContentSize(),
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = state is AllOffersStateUM.Content && state.currentMethod != null,
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn(tween(durationMillis = 220)) togetherWith
|
||||||
|
fadeOut(tween(durationMillis = 220))
|
||||||
|
},
|
||||||
|
label = "Change offers and payment method state",
|
||||||
|
) { shouldShowOffersScreen ->
|
||||||
|
when (state) {
|
||||||
|
AllOffersStateUM.Loading -> AllOffersContentLoading()
|
||||||
|
is AllOffersStateUM.Error -> AllOffersError(state.errorNotification)
|
||||||
|
is AllOffersStateUM.Content -> {
|
||||||
|
if (shouldShowOffersScreen) {
|
||||||
|
state.currentMethod?.let {
|
||||||
|
OffersBasedOnPaymentMethodContent(offers = it.offers)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PaymentMethodsContent(methods = state.methods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProviderTitle(onBackClick: () -> Unit, onCloseClick: () -> Unit) {
|
||||||
|
TangemModalBottomSheetTitle(
|
||||||
|
title = TextReference.Res(R.string.onramp_all_offers_button_title),
|
||||||
|
subtitle = TextReference.Res(R.string.express_choose_providers_title),
|
||||||
|
startIconRes = R.drawable.ic_back_24,
|
||||||
|
onStartClick = onBackClick,
|
||||||
|
endIconRes = com.tangem.core.ui.R.drawable.ic_close_24,
|
||||||
|
onEndClick = onCloseClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentMethodTitle(onCloseClick: () -> Unit) {
|
||||||
|
TangemModalBottomSheetTitle(
|
||||||
|
title = TextReference.Res(R.string.onramp_all_offers_button_title),
|
||||||
|
subtitle = TextReference.Res(R.string.onramp_payment_method_subtitle),
|
||||||
|
endIconRes = com.tangem.core.ui.R.drawable.ic_close_24,
|
||||||
|
onEndClick = onCloseClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OffersBasedOnPaymentMethodContent(offers: ImmutableList<OnrampOfferUM>) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
offers.fastForEach { offer ->
|
||||||
|
key("${offer.paymentMethod.id} ${offer.providerName} ${offer.rate}") {
|
||||||
|
Offer(offer)
|
||||||
|
SpacerH(8.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AllOffersContentLoading() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(TangemTheme.colors.background.tertiary)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(96.dp)
|
||||||
|
.size(width = 76.dp, height = 20.dp),
|
||||||
|
radius = 14.dp,
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(96.dp)
|
||||||
|
.size(width = 76.dp, height = 20.dp),
|
||||||
|
radius = 14.dp,
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(96.dp)
|
||||||
|
.size(width = 76.dp, height = 20.dp),
|
||||||
|
radius = 14.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AllOffersError(errorNotification: NotificationUM) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
Notification(config = errorNotification.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
|
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun AllOffersContentSheetPaymentPreview() {
|
||||||
|
val method = AllOffersPaymentMethodUM(
|
||||||
|
offers = persistentListOf(
|
||||||
|
OnrampOfferUM(
|
||||||
|
category = OnrampOfferCategoryUM.Recommended,
|
||||||
|
advantages = OnrampOfferAdvantagesUM.BestRate,
|
||||||
|
paymentMethod = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
providerId = "providerId1",
|
||||||
|
providerName = "Simplex",
|
||||||
|
rate = "0,0245334 BTC",
|
||||||
|
diff = null,
|
||||||
|
onBuyClicked = {},
|
||||||
|
),
|
||||||
|
OnrampOfferUM(
|
||||||
|
category = OnrampOfferCategoryUM.Recommended,
|
||||||
|
advantages = OnrampOfferAdvantagesUM.Default,
|
||||||
|
paymentMethod = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
providerId = "providerId2",
|
||||||
|
providerName = "Simplex",
|
||||||
|
rate = "0,00145334 BTC",
|
||||||
|
diff = stringReference("–0.07%"),
|
||||||
|
onBuyClicked = {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
methodConfig = OnrampPaymentMethodConfig(
|
||||||
|
method = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
diff = null,
|
||||||
|
rate = "0,0245334 BTC",
|
||||||
|
providersCount = 2,
|
||||||
|
isBestRate = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
TangemThemePreview {
|
||||||
|
AllOffersContentSheet(
|
||||||
|
state = AllOffersStateUM.Content(
|
||||||
|
methods = persistentListOf(),
|
||||||
|
currentMethod = method,
|
||||||
|
onBackClicked = {},
|
||||||
|
),
|
||||||
|
onCloseClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
|
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun AllOffersContentSheetOffersPreview() {
|
||||||
|
val methods = List(5) {
|
||||||
|
AllOffersPaymentMethodUM(
|
||||||
|
offers = persistentListOf(
|
||||||
|
OnrampOfferUM(
|
||||||
|
category = OnrampOfferCategoryUM.Recommended,
|
||||||
|
advantages = OnrampOfferAdvantagesUM.BestRate,
|
||||||
|
paymentMethod = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl =
|
||||||
|
"https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
providerId = "providerId1",
|
||||||
|
providerName = "Simplex",
|
||||||
|
rate = "0,0245334 BTC",
|
||||||
|
diff = null,
|
||||||
|
onBuyClicked = {},
|
||||||
|
),
|
||||||
|
OnrampOfferUM(
|
||||||
|
category = OnrampOfferCategoryUM.Recommended,
|
||||||
|
advantages = OnrampOfferAdvantagesUM.Default,
|
||||||
|
paymentMethod = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl =
|
||||||
|
"https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
providerId = "providerId2",
|
||||||
|
providerName = "Simplex",
|
||||||
|
rate = "0,00145334 BTC",
|
||||||
|
diff = stringReference("–0.07%"),
|
||||||
|
onBuyClicked = {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
methodConfig = OnrampPaymentMethodConfig(
|
||||||
|
method = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
diff = null,
|
||||||
|
rate = "0,0245334 BTC",
|
||||||
|
providersCount = 2,
|
||||||
|
isBestRate = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TangemThemePreview {
|
||||||
|
AllOffersContentSheet(
|
||||||
|
state = AllOffersStateUM.Content(
|
||||||
|
methods = methods.toPersistentList(),
|
||||||
|
currentMethod = null,
|
||||||
|
onBackClicked = {},
|
||||||
|
),
|
||||||
|
onCloseClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
package com.tangem.features.onramp.alloffers.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
import com.tangem.core.ui.components.SpacerW
|
||||||
|
import com.tangem.core.ui.extensions.*
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||||
|
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||||
|
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
||||||
|
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
|
||||||
|
import com.tangem.features.onramp.impl.R
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||||
|
import com.tangem.features.onramp.mainv2.ui.TimingBlock
|
||||||
|
import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun PaymentMethodsContent(methods: ImmutableList<AllOffersPaymentMethodUM>) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
methods.fastForEach { method ->
|
||||||
|
key(method.methodConfig.method.id) {
|
||||||
|
PaymentMethod(methodUM = method)
|
||||||
|
SpacerH(8.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentMethod(methodUM: AllOffersPaymentMethodUM, modifier: Modifier = Modifier) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = TangemTheme.colors.background.action,
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = ripple(),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = methodUM.methodConfig.onClick,
|
||||||
|
)
|
||||||
|
.padding(
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp,
|
||||||
|
top = 14.dp,
|
||||||
|
bottom = 12.dp,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
PaymentMethodIcon(
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
imageUrl = methodUM.methodConfig.method.imageUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerW(12.dp)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
PaymentMethodInfoBlock(
|
||||||
|
paymentMethodName = methodUM.methodConfig.method.name,
|
||||||
|
rate = methodUM.rate,
|
||||||
|
diff = methodUM.diff,
|
||||||
|
isBestRate = methodUM.isBestRate,
|
||||||
|
)
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
ProvidersCountBlockInfo(providersCount = methodUM.providersCount)
|
||||||
|
SpacerW(8.dp)
|
||||||
|
TimingBlockInfo(speed = methodUM.methodConfig.method.type.getProcessingSpeed())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentMethodInfoBlock(
|
||||||
|
paymentMethodName: String,
|
||||||
|
rate: String,
|
||||||
|
diff: TextReference?,
|
||||||
|
isBestRate: Boolean,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(bottom = 14.dp)) {
|
||||||
|
Text(
|
||||||
|
text = paymentMethodName,
|
||||||
|
style = TangemTheme.typography.subtitle2,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
SpacerH(2.dp)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.onramp_up_to_rate),
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = rate,
|
||||||
|
style = TangemTheme.typography.subtitle2,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
|
||||||
|
when {
|
||||||
|
isBestRate -> {
|
||||||
|
Image(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_best_rate_12),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
diff != null -> {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = TangemTheme.colors.text.warning.copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
)
|
||||||
|
.padding(horizontal = 4.dp),
|
||||||
|
text = diff.resolveReference(),
|
||||||
|
style = TangemTheme.typography.caption1,
|
||||||
|
color = TangemTheme.colors.text.warning,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProvidersCountBlockInfo(providersCount: Int) {
|
||||||
|
BorderedRow {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(10.dp),
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24),
|
||||||
|
tint = TangemTheme.colors.icon.informative,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
SpacerW(4.dp)
|
||||||
|
Text(
|
||||||
|
text = pluralStringResourceSafe(
|
||||||
|
id = R.plurals.onramp_providers_count,
|
||||||
|
count = providersCount,
|
||||||
|
providersCount,
|
||||||
|
),
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TimingBlockInfo(speed: PaymentMethodType.PaymentSpeed) {
|
||||||
|
BorderedRow {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(10.dp),
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_staking_24),
|
||||||
|
tint = TangemTheme.colors.icon.informative,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
SpacerW(4.dp)
|
||||||
|
TimingBlock(speed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BorderedRow(content: @Composable RowScope.() -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = TangemTheme.colors.stroke.primary,
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
)
|
||||||
|
.padding(horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PaymentMethodsContentPreview() {
|
||||||
|
val method = AllOffersPaymentMethodUM(
|
||||||
|
offers = persistentListOf(
|
||||||
|
OnrampOfferUM(
|
||||||
|
category = OnrampOfferCategoryUM.Recommended,
|
||||||
|
advantages = OnrampOfferAdvantagesUM.BestRate,
|
||||||
|
paymentMethod = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
providerId = "providerId1",
|
||||||
|
providerName = "Simplex",
|
||||||
|
rate = "0,0245334 BTC",
|
||||||
|
diff = null,
|
||||||
|
onBuyClicked = {},
|
||||||
|
),
|
||||||
|
OnrampOfferUM(
|
||||||
|
category = OnrampOfferCategoryUM.Recommended,
|
||||||
|
advantages = OnrampOfferAdvantagesUM.Default,
|
||||||
|
paymentMethod = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
providerId = "providerId2",
|
||||||
|
providerName = "Simplex",
|
||||||
|
rate = "0,00145334 BTC",
|
||||||
|
diff = stringReference("–0.07%"),
|
||||||
|
onBuyClicked = {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
methodConfig = OnrampPaymentMethodConfig(
|
||||||
|
method = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
diff = null,
|
||||||
|
rate = "0,0245334 BTC",
|
||||||
|
providersCount = 2,
|
||||||
|
isBestRate = true,
|
||||||
|
)
|
||||||
|
TangemThemePreview {
|
||||||
|
PaymentMethodsContent(persistentListOf(method))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,12 +69,14 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
|
||||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue