Updated on 2026-08-14
This commit is contained in:
parent
4197574fca
commit
7f587d170c
25 changed files with 348 additions and 571 deletions
|
|
@ -179,20 +179,6 @@ internal object OnrampDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetOnrampV2QuotesUseCase(
|
||||
settingsRepository: SettingsRepository,
|
||||
onrampRepository: OnrampRepository,
|
||||
onrampErrorResolver: OnrampErrorResolver,
|
||||
): GetOnrampV2QuotesUseCase {
|
||||
return GetOnrampV2QuotesUseCase(
|
||||
settingsRepository = settingsRepository,
|
||||
repository = onrampRepository,
|
||||
errorResolver = onrampErrorResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetOnrampProviderWithQuoteUseCase(
|
||||
|
|
|
|||
|
|
@ -918,7 +918,7 @@
|
|||
<item quantity="other">up to %d days</item>
|
||||
</plurals>
|
||||
<string name="onramp_timing_minutes">%s min</string>
|
||||
<string name="onramp_title_available_from___">Available from </string>
|
||||
<string name="onramp_title_available_from">Available from</string>
|
||||
<string name="onramp_title_you_get">You get</string>
|
||||
<string name="onramp_tos_external_providers">Service is provided by an external provider. \nTangem is not responsible.</string>
|
||||
<string name="onramp_transaction_status_footer_text">You can close this screen and check the transaction status on the token details screen.</string>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ data class OnrampOffer(
|
|||
)
|
||||
|
||||
enum class OnrampOfferAdvantages {
|
||||
Default, BestRate, Fastest,
|
||||
Default, BestRate, Fastest, GreatRate,
|
||||
}
|
||||
|
||||
enum class OnrampOfferCategory {
|
||||
|
|
|
|||
|
|
@ -18,31 +18,73 @@ enum class PaymentMethodType(val id: String?) {
|
|||
OTHER(id = null),
|
||||
;
|
||||
|
||||
// TODO will be removed in next request [REDACTED_TASK_KEY]
|
||||
@Suppress("MagicNumber")
|
||||
fun getPriority(isGooglePayEnabled: Boolean): Int = if (isGooglePayEnabled) {
|
||||
when (this) {
|
||||
GOOGLE_PAY -> 0
|
||||
CARD -> 1
|
||||
SEPA -> 2
|
||||
REVOLUT_PAY -> 3
|
||||
REVOLUT_PAY -> 2
|
||||
SEPA -> 3
|
||||
OTHER -> 4
|
||||
}
|
||||
} else {
|
||||
when (this) {
|
||||
CARD -> 0
|
||||
GOOGLE_PAY -> 1
|
||||
CARD -> 2
|
||||
REVOLUT_PAY -> 1
|
||||
SEPA -> 2
|
||||
REVOLUT_PAY -> 3
|
||||
OTHER -> 3
|
||||
GOOGLE_PAY -> 4
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority regardless of real speed. By business logic.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun getPriorityForMethod(isGooglePayEnabled: Boolean): Int = if (isGooglePayEnabled) {
|
||||
when (this) {
|
||||
GOOGLE_PAY -> 0
|
||||
CARD -> 1
|
||||
REVOLUT_PAY -> 2
|
||||
SEPA -> 3
|
||||
OTHER -> 4
|
||||
}
|
||||
} else {
|
||||
when (this) {
|
||||
CARD -> 2
|
||||
REVOLUT_PAY -> 1
|
||||
SEPA -> 2
|
||||
OTHER -> 3
|
||||
GOOGLE_PAY -> 4
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun getPriorityBySpeed(isGooglePayEnabled: Boolean): Int = if (isGooglePayEnabled) {
|
||||
when (this) {
|
||||
GOOGLE_PAY -> 0
|
||||
REVOLUT_PAY -> 1
|
||||
CARD -> 2
|
||||
SEPA -> 3
|
||||
OTHER -> 4
|
||||
}
|
||||
} else {
|
||||
when (this) {
|
||||
REVOLUT_PAY -> 0
|
||||
CARD -> 1
|
||||
SEPA -> 2
|
||||
OTHER -> 3
|
||||
GOOGLE_PAY -> 4
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BE AWARE. HARDCODED. Returns the speed of transaction for payment method type.
|
||||
*/
|
||||
fun getProcessingSpeed(): PaymentSpeed = when (this) {
|
||||
REVOLUT_PAY,
|
||||
GOOGLE_PAY,
|
||||
REVOLUT_PAY,
|
||||
-> PaymentSpeed.Instant
|
||||
CARD -> PaymentSpeed.FewMin
|
||||
SEPA -> PaymentSpeed.FewDays
|
||||
|
|
|
|||
|
|
@ -47,7 +47,12 @@ class GetOnrampOffersUseCase(
|
|||
if (validQuotes.isEmpty()) return emptyList()
|
||||
|
||||
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
|
||||
val bestRateQuote = validQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable))
|
||||
val bestRateQuote = validQuotes.maxWithOrNull(
|
||||
compareOffersByRateSpeedAndPriority(
|
||||
isGooglePayAvailable = isGooglePayAvailable,
|
||||
isSepaPrioritized = true,
|
||||
),
|
||||
)
|
||||
val bestRate = bestRateQuote?.toAmount?.value
|
||||
|
||||
val offers = validQuotes.map { quote ->
|
||||
|
|
@ -71,8 +76,9 @@ class GetOnrampOffersUseCase(
|
|||
val lastTransaction = transactions.maxByOrNull { it.timestamp } ?: return null
|
||||
|
||||
return offers.find { offer ->
|
||||
offer.quote.provider.id == lastTransaction.providerType &&
|
||||
offer.quote.paymentMethod.id == lastTransaction.paymentMethod
|
||||
offer.quote.provider.info.name == lastTransaction.providerName &&
|
||||
offer.quote.paymentMethod.name == lastTransaction.paymentMethod &&
|
||||
lastTransaction.status.isTerminal
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,14 +89,14 @@ class GetOnrampOffersUseCase(
|
|||
private fun findFastestOffer(offers: List<OnrampOffer>, isGooglePayAvailable: Boolean): OnrampOffer? {
|
||||
val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() }
|
||||
return if (instantOffers.isNotEmpty()) {
|
||||
instantOffers.maxWithOrNull(offerComparator(isGooglePayAvailable))
|
||||
instantOffers.maxWithOrNull(fastestOfferComparator(isGooglePayAvailable))
|
||||
} else {
|
||||
val offersBySpeed = offers.groupBy { offer ->
|
||||
offer.quote.paymentMethod.type.getProcessingSpeed().speed
|
||||
}
|
||||
val fastestSpeed = offersBySpeed.keys.minOrNull() ?: return null
|
||||
val fastestOffers = offersBySpeed[fastestSpeed] ?: return null
|
||||
fastestOffers.maxWithOrNull(offerComparator(isGooglePayAvailable))
|
||||
fastestOffers.maxWithOrNull(fastestOfferComparator(isGooglePayAvailable))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +105,10 @@ class GetOnrampOffersUseCase(
|
|||
is OnrampQuote.Data -> {
|
||||
when (val quote2 = offer2.quote) {
|
||||
is OnrampQuote.Data -> {
|
||||
compareOffersByRateSpeedAndPriority(isGooglePayAvailable).compare(quote1, quote2)
|
||||
compareOffersByRateSpeedAndPriority(
|
||||
isGooglePayAvailable = isGooglePayAvailable,
|
||||
isSepaPrioritized = true,
|
||||
).compare(quote1, quote2)
|
||||
}
|
||||
else -> 1
|
||||
}
|
||||
|
|
@ -108,6 +117,30 @@ class GetOnrampOffersUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
private fun fastestOfferComparator(isGooglePayAvailable: Boolean): Comparator<OnrampOffer> =
|
||||
Comparator { offer1, offer2 ->
|
||||
when (val quote1 = offer1.quote) {
|
||||
is OnrampQuote.Data -> {
|
||||
when (val quote2 = offer2.quote) {
|
||||
is OnrampQuote.Data -> {
|
||||
// For fastest offer, first compare by priority of speed
|
||||
val priorityComparison = quote2.paymentMethod.type.getPriorityBySpeed(isGooglePayAvailable)
|
||||
.compareTo(quote1.paymentMethod.type.getPriorityBySpeed(isGooglePayAvailable))
|
||||
|
||||
// If priorities are equal, compare by rate
|
||||
if (priorityComparison != 0) {
|
||||
priorityComparison
|
||||
} else {
|
||||
quote1.toAmount.value.compareTo(quote2.toAmount.value)
|
||||
}
|
||||
}
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildOffersBlocks(
|
||||
recentOffer: OnrampOffer?,
|
||||
bestRateOffer: OnrampOffer?,
|
||||
|
|
@ -143,7 +176,7 @@ class GetOnrampOffersUseCase(
|
|||
)
|
||||
}
|
||||
|
||||
if (recommendedOffers.isNotEmpty() && hasOnlyOneMethodAndProvider(allOffers).not()) {
|
||||
if (recommendedOffers.isNotEmpty()) {
|
||||
add(
|
||||
OnrampOffersBlock(
|
||||
category = OnrampOfferCategory.Recommended,
|
||||
|
|
@ -161,10 +194,10 @@ class GetOnrampOffersUseCase(
|
|||
fastestOffer: OnrampOffer?,
|
||||
): OnrampOfferAdvantages {
|
||||
if (isSameOffer(recentOffer, bestRateOffer) && isSameOffer(recentOffer, fastestOffer)) {
|
||||
return OnrampOfferAdvantages.BestRate
|
||||
return OnrampOfferAdvantages.GreatRate
|
||||
}
|
||||
if (isSameOffer(recentOffer, bestRateOffer)) {
|
||||
return OnrampOfferAdvantages.BestRate
|
||||
return OnrampOfferAdvantages.GreatRate
|
||||
}
|
||||
if (isSameOffer(recentOffer, fastestOffer)) {
|
||||
return OnrampOfferAdvantages.Fastest
|
||||
|
|
@ -182,7 +215,7 @@ class GetOnrampOffersUseCase(
|
|||
bestRateOffer?.let { offer ->
|
||||
add(
|
||||
offer.copy(
|
||||
advantages = OnrampOfferAdvantages.BestRate,
|
||||
advantages = OnrampOfferAdvantages.GreatRate,
|
||||
rateDif = null,
|
||||
),
|
||||
)
|
||||
|
|
@ -191,7 +224,7 @@ class GetOnrampOffersUseCase(
|
|||
if (bestRateOffer != null && !isSameOffer(bestRateOffer, recentOffer)) {
|
||||
add(
|
||||
bestRateOffer.copy(
|
||||
advantages = OnrampOfferAdvantages.BestRate,
|
||||
advantages = OnrampOfferAdvantages.GreatRate,
|
||||
rateDif = null,
|
||||
),
|
||||
)
|
||||
|
|
@ -211,12 +244,6 @@ class GetOnrampOffersUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
private fun hasOnlyOneMethodAndProvider(offers: List<OnrampOffer>): Boolean {
|
||||
val uniquePaymentMethods = offers.map { it.quote.paymentMethod.id }.distinct()
|
||||
val uniqueProviders = offers.map { it.quote.provider.id }.distinct()
|
||||
return uniquePaymentMethods.size == 1 && uniqueProviders.size == 1
|
||||
}
|
||||
|
||||
private fun isSameOffer(offer1: OnrampOffer?, offer2: OnrampOffer?): Boolean {
|
||||
if (offer1 == null || offer2 == null) return false
|
||||
return offer1.quote.provider.id == offer2.quote.provider.id &&
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class GetOnrampPaymentMethodsUseCase(
|
|||
|
||||
repository.getAvailablePaymentMethods()
|
||||
.toList()
|
||||
.sortedBy { it.type.getPriority(isGooglePayAvailable) }
|
||||
.sortedBy { it.type.getPriorityBySpeed(isGooglePayAvailable) }
|
||||
.toSet()
|
||||
}.mapLeft(errorResolver::resolve)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class GetOnrampQuotesUseCase(
|
|||
|
||||
quotes.groupBy { it.paymentMethod.type }
|
||||
.asSequence()
|
||||
.sortedBy { it.key.getPriority(isGooglePayAvailable) }
|
||||
.sortedBy { it.key.getPriorityBySpeed(isGooglePayAvailable) }
|
||||
.sortByRate()
|
||||
.toList()
|
||||
.flatten()
|
||||
|
|
@ -53,7 +53,6 @@ class GetOnrampQuotesUseCase(
|
|||
when (val error = it.error) {
|
||||
is OnrampError.AmountError.TooSmallError -> it.fromAmount.value - error.requiredAmount
|
||||
is OnrampError.AmountError.TooBigError -> error.requiredAmount - it.fromAmount.value
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
package com.tangem.domain.onramp
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigDecimal
|
||||
import java.util.Comparator
|
||||
|
||||
class GetOnrampV2QuotesUseCase(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val repository: OnrampRepository,
|
||||
private val errorResolver: OnrampErrorResolver,
|
||||
) {
|
||||
|
||||
operator fun invoke(): Flow<Either<OnrampError, List<OnrampQuote>>> {
|
||||
return repository.getQuotes()
|
||||
.map<List<OnrampQuote>, Either<OnrampError, List<OnrampQuote>>> { quotes ->
|
||||
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
|
||||
quotes.sortedWith(
|
||||
Comparator.comparing<OnrampQuote, SortableBigDecimalWrapper> { quote ->
|
||||
getQuoteSortPriority(quote)
|
||||
}
|
||||
.thenComparingInt { quote ->
|
||||
quote.paymentMethod.type.getPriority(isGooglePayAvailable)
|
||||
},
|
||||
).right()
|
||||
}
|
||||
.catch {
|
||||
emit(errorResolver.resolve(it).left())
|
||||
}
|
||||
}
|
||||
|
||||
private class SortableBigDecimalWrapper(
|
||||
val value: BigDecimal?,
|
||||
val negateForSort: Boolean = false,
|
||||
) : Comparable<SortableBigDecimalWrapper> {
|
||||
override fun compareTo(other: SortableBigDecimalWrapper): Int {
|
||||
return when {
|
||||
value == null && other.value == null -> 0
|
||||
value == null -> 1
|
||||
other.value == null -> -1
|
||||
else -> {
|
||||
val thisValue = if (negateForSort) value.negate() else value
|
||||
val otherValue = if (other.negateForSort) other.value.negate() else other.value
|
||||
thisValue.compareTo(otherValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getQuoteSortPriority(quote: OnrampQuote): SortableBigDecimalWrapper {
|
||||
return when (quote) {
|
||||
is OnrampQuote.Data -> SortableBigDecimalWrapper(quote.toAmount.value, negateForSort = true)
|
||||
is OnrampQuote.Error -> SortableBigDecimalWrapper(null)
|
||||
is OnrampQuote.AmountError -> {
|
||||
when (val error = quote.error) {
|
||||
is OnrampError.AmountError.TooSmallError ->
|
||||
SortableBigDecimalWrapper((quote.fromAmount.value - error.requiredAmount).abs())
|
||||
is OnrampError.AmountError.TooBigError ->
|
||||
SortableBigDecimalWrapper((error.requiredAmount - quote.fromAmount.value).abs())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,42 @@
|
|||
package com.tangem.domain.onramp.utils
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal fun calculateRateDif(currentTokenRate: BigDecimal, bestRate: BigDecimal?): BigDecimal? {
|
||||
if (bestRate == null) return null
|
||||
return BigDecimal.ONE - currentTokenRate / bestRate
|
||||
if (currentTokenRate > bestRate) return null
|
||||
|
||||
val rateDif = BigDecimal.ONE - currentTokenRate / bestRate
|
||||
return if (rateDif >= BigDecimal("0.01")) rateDif else null
|
||||
}
|
||||
|
||||
internal fun compareOffersByRateSpeedAndPriority(isGooglePayAvailable: Boolean): Comparator<OnrampQuote.Data> {
|
||||
/**
|
||||
* @param isSepaPrioritized Sepa provider should be prioritized over all offers no matter what.
|
||||
*/
|
||||
internal fun compareOffersByRateSpeedAndPriority(
|
||||
isGooglePayAvailable: Boolean,
|
||||
isSepaPrioritized: Boolean = false,
|
||||
): Comparator<OnrampQuote.Data> {
|
||||
return Comparator { quote1, quote2 ->
|
||||
val rateComparison = quote1
|
||||
.toAmount
|
||||
.value
|
||||
.compareTo(quote2.toAmount.value)
|
||||
if (isSepaPrioritized) {
|
||||
val isQuote1Sepa = quote1.paymentMethod.type == PaymentMethodType.SEPA
|
||||
val isQuote2Sepa = quote2.paymentMethod.type == PaymentMethodType.SEPA
|
||||
|
||||
if (isQuote1Sepa && !isQuote2Sepa) return@Comparator 1
|
||||
if (!isQuote1Sepa && isQuote2Sepa) return@Comparator -1
|
||||
}
|
||||
|
||||
val rateComparison = quote1.toAmount.value.compareTo(quote2.toAmount.value)
|
||||
if (rateComparison != 0) return@Comparator rateComparison
|
||||
|
||||
val speedComparison =
|
||||
quote2
|
||||
.paymentMethod
|
||||
.type
|
||||
.getProcessingSpeed()
|
||||
.speed
|
||||
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))
|
||||
quote2.paymentMethod.type.getPriorityBySpeed(isGooglePayAvailable)
|
||||
.compareTo(quote1.paymentMethod.type.getPriorityBySpeed(isGooglePayAvailable))
|
||||
}
|
||||
}
|
||||
|
|
@ -67,8 +67,8 @@ class GetOnrampOffersUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `invoke should return offers blocks with recent and recommended categories`() = runTest {
|
||||
val paymentMethod1 = createMockPaymentMethod("card", "Card", isInstant = true)
|
||||
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false)
|
||||
val paymentMethod1 = createMockPaymentMethod("card", "Card", PaymentMethodType.GOOGLE_PAY)
|
||||
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD)
|
||||
val provider1 = createMockProvider("provider1", "Provider 1")
|
||||
val provider2 = createMockProvider("provider2", "Provider 2")
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ class GetOnrampOffersUseCaseTest {
|
|||
)
|
||||
|
||||
val transactions = listOf(
|
||||
createMockTransaction("provider1", "card", 1000L),
|
||||
createMockTransaction("Provider 1", "Card", 1000L),
|
||||
)
|
||||
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns false
|
||||
|
|
@ -105,7 +105,7 @@ class GetOnrampOffersUseCaseTest {
|
|||
Truth.assertThat(recommendedBlock).isNotNull()
|
||||
Truth.assertThat(recommendedBlock?.offers).hasSize(1)
|
||||
Truth.assertThat(recommendedBlock?.offers?.first()?.advantages)
|
||||
.isEqualTo(OnrampOfferAdvantages.BestRate)
|
||||
.isEqualTo(OnrampOfferAdvantages.GreatRate)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -113,8 +113,8 @@ class GetOnrampOffersUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `invoke should find best rate offer correctly`() = runTest {
|
||||
val paymentMethod1 = createMockPaymentMethod("card", "Card", isInstant = false)
|
||||
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false)
|
||||
val paymentMethod1 = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD)
|
||||
val provider1 = createMockProvider("provider1", "Provider 1")
|
||||
val provider2 = createMockProvider("provider2", "Provider 2")
|
||||
|
||||
|
|
@ -145,10 +145,10 @@ class GetOnrampOffersUseCaseTest {
|
|||
Truth.assertThat(recommendedBlock).isNotNull()
|
||||
Truth.assertThat(recommendedBlock?.offers).hasSize(1)
|
||||
|
||||
val bestRateOffer = recommendedBlock?.offers?.first()
|
||||
Truth.assertThat(bestRateOffer?.advantages).isEqualTo(OnrampOfferAdvantages.BestRate)
|
||||
val grateRateOffer = recommendedBlock?.offers?.first()
|
||||
Truth.assertThat(grateRateOffer?.advantages).isEqualTo(OnrampOfferAdvantages.GreatRate)
|
||||
|
||||
when (val quote = bestRateOffer?.quote) {
|
||||
when (val quote = grateRateOffer?.quote) {
|
||||
is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0"))
|
||||
else -> Truth.assertThat(false).isTrue()
|
||||
}
|
||||
|
|
@ -159,8 +159,10 @@ class GetOnrampOffersUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `invoke should find fastest offer correctly`() = runTest {
|
||||
val instantPaymentMethod = createMockPaymentMethod("card", "Card", isInstant = true)
|
||||
val slowPaymentMethod = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false)
|
||||
val instantPaymentMethod =
|
||||
createMockPaymentMethod("card", "Card", PaymentMethodType.GOOGLE_PAY)
|
||||
val slowPaymentMethod =
|
||||
createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD)
|
||||
val provider1 = createMockProvider("provider1", "Provider 1")
|
||||
val provider2 = createMockProvider("provider2", "Provider 2")
|
||||
|
||||
|
|
@ -190,17 +192,17 @@ class GetOnrampOffersUseCaseTest {
|
|||
Truth.assertThat(recommendedBlock).isNotNull()
|
||||
Truth.assertThat(recommendedBlock?.offers).hasSize(2)
|
||||
|
||||
val bestRateOffer = recommendedBlock
|
||||
val grateRateOffer = recommendedBlock
|
||||
?.offers
|
||||
?.find { it.advantages == OnrampOfferAdvantages.BestRate }
|
||||
?.find { it.advantages == OnrampOfferAdvantages.GreatRate }
|
||||
val fastestOffer = recommendedBlock
|
||||
?.offers
|
||||
?.find { it.advantages == OnrampOfferAdvantages.Fastest }
|
||||
|
||||
Truth.assertThat(bestRateOffer).isNotNull()
|
||||
Truth.assertThat(grateRateOffer).isNotNull()
|
||||
Truth.assertThat(fastestOffer).isNotNull()
|
||||
|
||||
when (val quote = bestRateOffer?.quote) {
|
||||
when (val quote = grateRateOffer?.quote) {
|
||||
is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0"))
|
||||
else -> Truth.assertThat(false).isTrue()
|
||||
}
|
||||
|
|
@ -215,8 +217,8 @@ class GetOnrampOffersUseCaseTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should not show recommended block when only one method and provider`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card", isInstant = false)
|
||||
fun `invoke should show recommended block when only one method and provider`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val provider = createMockProvider("provider1", "Provider 1")
|
||||
|
||||
val quotes = listOf(
|
||||
|
|
@ -238,22 +240,22 @@ class GetOnrampOffersUseCaseTest {
|
|||
either.fold(
|
||||
ifLeft = { error -> Truth.assertThat(error).isNull() },
|
||||
ifRight = { offers ->
|
||||
Truth.assertThat(offers).isEmpty()
|
||||
Truth.assertThat(offers).isNotEmpty()
|
||||
Truth.assertThat(offers).hasSize(1)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMockPaymentMethod(id: String, name: String, isInstant: Boolean): OnrampPaymentMethod {
|
||||
private fun createMockPaymentMethod(
|
||||
id: String,
|
||||
name: String,
|
||||
type: PaymentMethodType = PaymentMethodType.CARD,
|
||||
): OnrampPaymentMethod {
|
||||
return mockk<OnrampPaymentMethod> {
|
||||
every { this@mockk.id } returns id
|
||||
every { this@mockk.name } returns name
|
||||
every { this@mockk.type } returns mockk {
|
||||
every { isInstant() } returns isInstant
|
||||
every { getProcessingSpeed() } returns mockk {
|
||||
every { speed } returns if (isInstant) 1 else 3
|
||||
}
|
||||
}
|
||||
every { this@mockk.type } returns type
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,11 +280,12 @@ class GetOnrampOffersUseCaseTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createMockTransaction(providerType: String, paymentMethod: String, timestamp: Long): OnrampTransaction {
|
||||
private fun createMockTransaction(providerName: String, paymentMethod: String, timestamp: Long): OnrampTransaction {
|
||||
return mockk<OnrampTransaction> {
|
||||
every { this@mockk.providerType } returns providerType
|
||||
every { this@mockk.providerName } returns providerName
|
||||
every { this@mockk.paymentMethod } returns paymentMethod
|
||||
every { this@mockk.timestamp } returns timestamp
|
||||
every { this@mockk.status.isTerminal } returns true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,6 +108,7 @@ internal class AllOffersStateFactory(
|
|||
OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default
|
||||
OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate
|
||||
OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest
|
||||
OnrampOfferAdvantages.GreatRate -> OnrampOfferAdvantagesUM.Default
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +122,6 @@ internal class AllOffersStateFactory(
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -204,7 +204,6 @@ private fun AllOffersContentSheetPaymentPreview() {
|
|||
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,
|
||||
|
|
@ -219,7 +218,6 @@ private fun AllOffersContentSheetPaymentPreview() {
|
|||
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%"),
|
||||
|
|
@ -270,7 +268,6 @@ private fun AllOffersContentSheetOffersPreview() {
|
|||
"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,
|
||||
|
|
@ -286,7 +283,6 @@ private fun AllOffersContentSheetOffersPreview() {
|
|||
"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%"),
|
||||
|
|
|
|||
|
|
@ -234,7 +234,6 @@ private fun PaymentMethodsContentPreview() {
|
|||
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,
|
||||
|
|
@ -249,7 +248,6 @@ private fun PaymentMethodsContentPreview() {
|
|||
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%"),
|
||||
|
|
|
|||
|
|
@ -9,19 +9,11 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
@Immutable
|
||||
internal sealed interface OnrampOffersBlockUM {
|
||||
|
||||
val isBlockVisible: Boolean
|
||||
data object Empty : OnrampOffersBlockUM
|
||||
|
||||
data object Empty : OnrampOffersBlockUM {
|
||||
override val isBlockVisible: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
data class Loading(
|
||||
override val isBlockVisible: Boolean,
|
||||
) : OnrampOffersBlockUM
|
||||
data object Loading : OnrampOffersBlockUM
|
||||
|
||||
data class Content(
|
||||
override val isBlockVisible: Boolean,
|
||||
val recentOffer: OnrampOfferUM?,
|
||||
val recommended: ImmutableList<OnrampOfferUM>,
|
||||
val onrampAllOffersButtonConfig: OnrampAllOffersButtonConfig?,
|
||||
|
|
@ -32,7 +24,6 @@ internal data class OnrampOfferUM(
|
|||
val category: OnrampOfferCategoryUM,
|
||||
val advantages: OnrampOfferAdvantagesUM,
|
||||
val paymentMethod: OnrampPaymentMethod,
|
||||
val providerId: String,
|
||||
val providerName: String,
|
||||
val rate: String,
|
||||
val diff: TextReference?,
|
||||
|
|
@ -44,7 +35,7 @@ internal enum class OnrampOfferCategoryUM {
|
|||
}
|
||||
|
||||
internal enum class OnrampOfferAdvantagesUM {
|
||||
Default, BestRate, Fastest;
|
||||
Default, BestRate, GreatRate, Fastest, Unavailable;
|
||||
|
||||
fun toAnalyticsEvent(
|
||||
cryptoCurrencySymbol: String,
|
||||
|
|
@ -52,7 +43,7 @@ internal enum class OnrampOfferAdvantagesUM {
|
|||
paymentMethodName: String,
|
||||
): OnrampAnalyticsEvent? {
|
||||
return when (this) {
|
||||
BestRate -> OnrampAnalyticsEvent.BestRateClicked(
|
||||
GreatRate -> OnrampAnalyticsEvent.BestRateClicked(
|
||||
tokenSymbol = cryptoCurrencySymbol,
|
||||
providerName = providerName,
|
||||
paymentMethod = paymentMethodName,
|
||||
|
|
@ -62,7 +53,10 @@ internal enum class OnrampOfferAdvantagesUM {
|
|||
providerName = providerName,
|
||||
paymentMethod = paymentMethodName,
|
||||
)
|
||||
Default -> null
|
||||
Default,
|
||||
BestRate,
|
||||
Unavailable,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
internal data class OnrampNewAmountBlockUM(
|
||||
val currencyUM: OnrampNewCurrencyUM,
|
||||
val amountFieldModel: AmountFieldModel,
|
||||
val secondaryFieldModel: OnrampNewAmountSecondaryFieldUM,
|
||||
val secondaryFieldModel: OnrampSecondaryFieldErrorUM,
|
||||
)
|
||||
|
||||
internal data class OnrampNewCurrencyUM(
|
||||
|
|
@ -20,10 +20,9 @@ internal data class OnrampNewCurrencyUM(
|
|||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface OnrampNewAmountSecondaryFieldUM {
|
||||
data object Loading : OnrampNewAmountSecondaryFieldUM
|
||||
data class Content(val amount: TextReference) : OnrampNewAmountSecondaryFieldUM
|
||||
data class Error(val error: TextReference) : OnrampNewAmountSecondaryFieldUM
|
||||
internal sealed interface OnrampSecondaryFieldErrorUM {
|
||||
data object Empty : OnrampSecondaryFieldErrorUM
|
||||
data class Error(val error: TextReference) : OnrampSecondaryFieldErrorUM
|
||||
}
|
||||
|
||||
internal sealed interface OnrampV2AmountButtonUMState {
|
||||
|
|
|
|||
|
|
@ -9,5 +9,4 @@ internal interface OnrampV2Intents {
|
|||
fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM)
|
||||
fun openProviders()
|
||||
fun onRefresh()
|
||||
fun onContinueClick()
|
||||
}
|
||||
|
|
@ -9,33 +9,22 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
internal sealed interface OnrampV2MainComponentUM {
|
||||
|
||||
val topBarConfig: OnrampV2MainTopBarUM
|
||||
val continueButtonConfig: ContinueButtonUM
|
||||
val errorNotification: NotificationUM?
|
||||
|
||||
data class InitialLoading(
|
||||
override val topBarConfig: OnrampV2MainTopBarUM,
|
||||
override val continueButtonConfig: ContinueButtonUM,
|
||||
override val errorNotification: NotificationUM?,
|
||||
) : OnrampV2MainComponentUM
|
||||
|
||||
data class Content(
|
||||
override val topBarConfig: OnrampV2MainTopBarUM,
|
||||
override val continueButtonConfig: ContinueButtonUM,
|
||||
override val errorNotification: NotificationUM?,
|
||||
val amountBlockState: OnrampNewAmountBlockUM,
|
||||
val offersBlockState: OnrampOffersBlockUM,
|
||||
val onrampAmountButtonUMState: OnrampV2AmountButtonUMState,
|
||||
val onrampProviderState: OnrampV2ProvidersUM,
|
||||
) : OnrampV2MainComponentUM
|
||||
}
|
||||
|
||||
internal data class ContinueButtonUM(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
val enabled: Boolean,
|
||||
val showProgress: Boolean = false,
|
||||
)
|
||||
|
||||
internal data class OnrampV2MainTopBarUM(
|
||||
val title: TextReference,
|
||||
val startButtonUM: TopAppBarButtonUM,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,7 @@ package com.tangem.features.onramp.mainv2.entity.converter
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
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.utils.parseBigDecimalOrNull
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
||||
import com.tangem.utils.Provider
|
||||
|
|
@ -18,7 +14,6 @@ internal class OnrampV2AmountFieldChangeConverter(
|
|||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
private val onrampIntents: OnrampV2Intents,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
) : Converter<String, OnrampV2MainComponentUM> {
|
||||
|
||||
override fun convert(value: String): OnrampV2MainComponentUM {
|
||||
|
|
@ -39,12 +34,11 @@ internal class OnrampV2AmountFieldChangeConverter(
|
|||
return state.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
amountFieldModel = amountFieldModel,
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading,
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
continueButtonConfig = state.continueButtonConfig.copy(enabled = false),
|
||||
onrampProviderState = OnrampV2ProvidersUM.Loading,
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
offersBlockState = OnrampOffersBlockUM.Loading,
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -63,22 +57,14 @@ internal class OnrampV2AmountFieldChangeConverter(
|
|||
return copy(
|
||||
amountBlockState = amountBlockState.copy(
|
||||
amountFieldModel = amountFieldModel,
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content(
|
||||
stringReference(
|
||||
BigDecimal.ZERO.format {
|
||||
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||
},
|
||||
),
|
||||
),
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
continueButtonConfig = continueButtonConfig.copy(enabled = false),
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||
currencySymbol = amountBlockState.currencyUM.unit,
|
||||
currencyCode = amountBlockState.currencyUM.code,
|
||||
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||
),
|
||||
onrampProviderState = OnrampV2ProvidersUM.Empty,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,15 +17,12 @@ internal class OnrampOffersStateFactory(
|
|||
private val onrampIntents: OnrampV2Intents,
|
||||
) {
|
||||
|
||||
fun getOnShowOffersState(offers: List<OnrampOffersBlock>): OnrampV2MainComponentUM {
|
||||
fun getOffersState(offers: List<OnrampOffersBlock>): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider.invoke()
|
||||
return when (currentState) {
|
||||
is OnrampV2MainComponentUM.Content -> {
|
||||
currentState.copy(
|
||||
offersBlockState = mapOnrampOffersBlockToUM(
|
||||
offersBlocks = offers,
|
||||
currentState = currentState,
|
||||
),
|
||||
offersBlockState = mapOnrampOffersBlockToUM(offersBlocks = offers),
|
||||
)
|
||||
}
|
||||
is OnrampV2MainComponentUM.InitialLoading -> {
|
||||
|
|
@ -34,10 +31,7 @@ internal class OnrampOffersStateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun mapOnrampOffersBlockToUM(
|
||||
offersBlocks: List<OnrampOffersBlock>,
|
||||
currentState: OnrampV2MainComponentUM.Content,
|
||||
): OnrampOffersBlockUM.Content {
|
||||
private fun mapOnrampOffersBlockToUM(offersBlocks: List<OnrampOffersBlock>): OnrampOffersBlockUM {
|
||||
val allOffersUM = mutableListOf<OnrampOfferUM>()
|
||||
offersBlocks.map { block ->
|
||||
block.offers.forEach { offer ->
|
||||
|
|
@ -51,7 +45,6 @@ internal class OnrampOffersStateFactory(
|
|||
category = mapOfferCategoryDTOtoUM(block.category),
|
||||
advantages = mapOfferAdvantagesDTOtoUM(offer.advantages),
|
||||
paymentMethod = currentQuote.paymentMethod,
|
||||
providerId = currentQuote.provider.id,
|
||||
providerName = currentQuote.provider.info.name,
|
||||
rate = currentQuote.toAmount.value.format {
|
||||
crypto(
|
||||
|
|
@ -80,8 +73,9 @@ internal class OnrampOffersStateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
if (allOffersUM.isEmpty()) return OnrampOffersBlockUM.Empty
|
||||
|
||||
return OnrampOffersBlockUM.Content(
|
||||
isBlockVisible = currentState.offersBlockState.isBlockVisible,
|
||||
recentOffer = allOffersUM.firstOrNull { it.category == OnrampOfferCategoryUM.RecentlyUsed },
|
||||
recommended = allOffersUM.filter { it.category == OnrampOfferCategoryUM.Recommended }.toPersistentList(),
|
||||
onrampAllOffersButtonConfig = if (offersBlocks.any { it.hasMoreOffers }) {
|
||||
|
|
@ -107,6 +101,7 @@ internal class OnrampOffersStateFactory(
|
|||
OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default
|
||||
OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate
|
||||
OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest
|
||||
OnrampOfferAdvantages.GreatRate -> OnrampOfferAdvantagesUM.GreatRate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
|
|
@ -18,13 +14,11 @@ import com.tangem.features.onramp.impl.R
|
|||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.converter.OnrampV2AmountFieldChangeConverter
|
||||
import com.tangem.utils.Provider
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class OnrampV2AmountStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val onrampIntents: OnrampV2Intents,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
) {
|
||||
|
||||
|
|
@ -35,7 +29,6 @@ internal class OnrampV2AmountStateFactory(
|
|||
currentStateProvider = currentStateProvider,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
onrampIntents = onrampIntents,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -74,127 +67,41 @@ internal class OnrampV2AmountStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryLoadingState(): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading,
|
||||
),
|
||||
offersBlockState = OnrampOffersBlockUM.Loading(
|
||||
isBlockVisible = currentState.offersBlockState.isBlockVisible,
|
||||
),
|
||||
continueButtonConfig = currentState.continueButtonConfig.copy(enabled = false),
|
||||
errorNotification = null,
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
onrampProviderState = OnrampV2ProvidersUM.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampV2MainComponentUM {
|
||||
fun getAmountSecondaryFieldUpdatedState(quotes: List<OnrampQuote>): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState
|
||||
|
||||
val limitedQuote = getLimitFromAmountErrors(quotes)
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
amountFieldModel = amountState.amountFieldModel.copy(isError = false),
|
||||
secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel,
|
||||
),
|
||||
continueButtonConfig = currentState.continueButtonConfig.copy(
|
||||
enabled = quote is OnrampQuote.Data,
|
||||
onClick = onrampIntents::onContinueClick,
|
||||
secondaryFieldModel = limitedQuote?.toSecondaryFieldUiModel(amountState)
|
||||
?: OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getUpdatedProviderState(selectedQuote: OnrampQuote): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ProviderCalculated(
|
||||
providerName = selectedQuote.provider.info.name,
|
||||
tokenSymbol = cryptoCurrency.symbol,
|
||||
paymentMethod = selectedQuote.paymentMethod.name,
|
||||
),
|
||||
)
|
||||
return currentState.copy(
|
||||
onrampProviderState = selectedQuote.toProviderBlockState(),
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryResetState(): OnrampV2MainComponentUM {
|
||||
fun getAmountSecondaryFieldResetState(): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
if (amountState.secondaryFieldModel is OnrampNewAmountSecondaryFieldUM.Content) return currentState
|
||||
if (amountState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Empty) return currentState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content(
|
||||
amount = stringReference(
|
||||
BigDecimal.ZERO.format {
|
||||
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
amountBlockState = amountState.copy(secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getShowProvidersState(): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
return when (currentState.offersBlockState) {
|
||||
is OnrampOffersBlockUM.Content -> {
|
||||
currentState.copy(
|
||||
offersBlockState = currentState.offersBlockState.copy(isBlockVisible = true),
|
||||
)
|
||||
}
|
||||
OnrampOffersBlockUM.Empty,
|
||||
is OnrampOffersBlockUM.Loading,
|
||||
-> currentState
|
||||
}
|
||||
}
|
||||
|
||||
private fun OnrampQuote.toProviderBlockState(): OnrampV2ProvidersUM {
|
||||
return OnrampV2ProvidersUM.Content(
|
||||
paymentMethod = paymentMethod,
|
||||
providerId = provider.id,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampQuote.toSecondaryFieldUiModel(
|
||||
amountState: OnrampNewAmountBlockUM,
|
||||
): OnrampNewAmountSecondaryFieldUM? {
|
||||
return when (this) {
|
||||
is OnrampQuote.Error -> null
|
||||
is OnrampQuote.Data -> {
|
||||
val amount = toAmount.value.format {
|
||||
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||
}
|
||||
val contentAmount = combinedReference(stringReference("\u007E"), stringReference(amount))
|
||||
OnrampNewAmountSecondaryFieldUM.Content(contentAmount)
|
||||
}
|
||||
is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun OnrampQuote.AmountError.toSecondaryFieldUiModel(
|
||||
amountState: OnrampNewAmountBlockUM,
|
||||
): OnrampNewAmountSecondaryFieldUM.Error {
|
||||
): OnrampSecondaryFieldErrorUM.Error {
|
||||
val amount = error.requiredAmount.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
|
||||
|
|
@ -213,11 +120,27 @@ internal class OnrampV2AmountStateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
return OnrampNewAmountSecondaryFieldUM.Error(
|
||||
return OnrampSecondaryFieldErrorUM.Error(
|
||||
resourceReference(
|
||||
errorTextRes,
|
||||
wrappedList(amount),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLimitFromAmountErrors(quotes: List<OnrampQuote>): OnrampQuote.AmountError? {
|
||||
val amountErrorQuotes = quotes.filterIsInstance<OnrampQuote.AmountError>()
|
||||
if (amountErrorQuotes.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val tooSmallErrors = amountErrorQuotes.filter { it.error is OnrampError.AmountError.TooSmallError }
|
||||
val tooBigErrors = amountErrorQuotes.filter { it.error is OnrampError.AmountError.TooBigError }
|
||||
if (tooSmallErrors.isNotEmpty()) {
|
||||
return tooSmallErrors.minByOrNull { it.error.requiredAmount }
|
||||
}
|
||||
if (tooBigErrors.isNotEmpty()) {
|
||||
return tooBigErrors.maxByOrNull { it.error.requiredAmount }
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,6 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
|
|
@ -50,11 +48,6 @@ internal class OnrampV2StateFactory(
|
|||
isEnabled = false,
|
||||
),
|
||||
),
|
||||
continueButtonConfig = ContinueButtonUM(
|
||||
text = resourceReference(R.string.common_continue),
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -70,11 +63,6 @@ internal class OnrampV2StateFactory(
|
|||
|
||||
return OnrampV2MainComponentUM.Content(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
continueButtonConfig = ContinueButtonUM(
|
||||
text = resourceReference(R.string.common_continue),
|
||||
onClick = onrampIntents::onContinueClick,
|
||||
enabled = false,
|
||||
),
|
||||
amountBlockState = initialAmountBlockState,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = null,
|
||||
|
|
@ -83,7 +71,6 @@ internal class OnrampV2StateFactory(
|
|||
currencySymbol = currency.unit,
|
||||
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||
),
|
||||
onrampProviderState = OnrampV2ProvidersUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -103,23 +90,6 @@ internal class OnrampV2StateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNoPairsErrorState(): OnrampV2MainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val contentState = state as? OnrampV2MainComponentUM.Content ?: return state
|
||||
|
||||
return contentState.copy(
|
||||
continueButtonConfig = contentState.continueButtonConfig.copy(enabled = false),
|
||||
amountBlockState = contentState.amountBlockState.copy(
|
||||
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Error(
|
||||
error = resourceReference(R.string.onramp_no_available_providers),
|
||||
),
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampV2MainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||
|
|
@ -130,23 +100,15 @@ internal class OnrampV2StateFactory(
|
|||
return when (state) {
|
||||
is OnrampV2MainComponentUM.Content -> state.copy(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
continueButtonConfig = state.continueButtonConfig.copy(enabled = false),
|
||||
amountBlockState = state.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content(
|
||||
stringReference(
|
||||
BigDecimal.ZERO.format {
|
||||
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
onrampProviderState = OnrampV2ProvidersUM.Empty,
|
||||
amountBlockState = state.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
)
|
||||
is OnrampV2MainComponentUM.InitialLoading -> state.copy(
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
|
|
@ -157,6 +119,22 @@ internal class OnrampV2StateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNoPairsErrorState(): OnrampV2MainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val contentState = state as? OnrampV2MainComponentUM.Content ?: return state
|
||||
|
||||
return contentState.copy(
|
||||
amountBlockState = contentState.amountBlockState.copy(
|
||||
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error(
|
||||
error = resourceReference(R.string.onramp_no_available_providers),
|
||||
),
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampNewAmountBlockUM {
|
||||
return OnrampNewAmountBlockUM(
|
||||
currencyUM = OnrampNewCurrencyUM(
|
||||
|
|
@ -185,13 +163,7 @@ internal class OnrampV2StateFactory(
|
|||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = {},
|
||||
),
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content(
|
||||
stringReference(
|
||||
BigDecimal.ZERO.format {
|
||||
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||
},
|
||||
),
|
||||
),
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.features.onramp.mainv2.model
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -15,7 +14,6 @@ import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
|||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.main.entity.OnrampLastUpdate
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
||||
|
|
@ -42,7 +40,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
|
||||
private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase,
|
||||
private val fetchQuotesUseCase: OnrampFetchQuotesUseCase,
|
||||
private val getOnrampQuotesUseCase: GetOnrampV2QuotesUseCase,
|
||||
private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase,
|
||||
private val fetchPairsUseCase: OnrampFetchPairsUseCase,
|
||||
private val amountInputManager: InputManager,
|
||||
private val getOnrampOffersUseCase: GetOnrampOffersUseCase,
|
||||
|
|
@ -52,8 +50,6 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
|
||||
val params = paramsContainer.require<OnrampV2MainComponent.Params>()
|
||||
|
||||
private val lastUpdateState = mutableStateOf<OnrampLastUpdate?>(null)
|
||||
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampAmountButtonUMStateFactory()
|
||||
}
|
||||
|
|
@ -65,20 +61,23 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private val stateFactory = OnrampV2StateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
private val stateFactory: OnrampV2StateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampV2StateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
}
|
||||
|
||||
private val amountStateFactory = OnrampV2AmountStateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onrampIntents = this,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
private val amountStateFactory: OnrampV2AmountStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampV2AmountStateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
}
|
||||
|
||||
private val _state: MutableStateFlow<OnrampV2MainComponentUM> = MutableStateFlow(
|
||||
value = stateFactory.getInitialState(
|
||||
|
|
@ -97,7 +96,6 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
clearOnrampCacheUseCase()
|
||||
}
|
||||
|
||||
sendScreenOpenAnalytics()
|
||||
checkResidenceCountry()
|
||||
subscribeToAmountChanges()
|
||||
|
|
@ -139,7 +137,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
cryptoCurrencySymbol = params.cryptoCurrency.symbol,
|
||||
providerName = quote.provider.info.name,
|
||||
paymentMethodName = quote.paymentMethod.name,
|
||||
)?.let { analyticsEventHandler::send }
|
||||
)?.let(analyticsEventHandler::send)
|
||||
params.openRedirectPage(quote)
|
||||
}
|
||||
|
||||
|
|
@ -160,27 +158,23 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
clearOnrampCacheUseCase.invoke()
|
||||
checkResidenceCountry()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContinueClick() {
|
||||
val currentState = _state.value
|
||||
if (currentState is OnrampV2MainComponentUM.Content) {
|
||||
_state.update { amountStateFactory.getShowProvidersState() }
|
||||
handleOnrampAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
quotesTaskScheduler.scheduleTask(
|
||||
scope = modelScope,
|
||||
task = loadQuotesTask(),
|
||||
)
|
||||
startLoadingQuotes()
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
quotesTaskScheduler.cancelTask()
|
||||
}
|
||||
|
||||
fun handleOnrampAvailable() {
|
||||
subscribeToCountryAndCurrencyUpdates()
|
||||
subscribeToQuotesUpdate()
|
||||
}
|
||||
|
||||
private fun startLoadingQuotes() {
|
||||
quotesTaskScheduler.cancelTask()
|
||||
quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask())
|
||||
|
|
@ -235,7 +229,11 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
maybeOffers.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { offers ->
|
||||
_state.update { onrampOffersStateFactory.getOnShowOffersState(offers) }
|
||||
if (offers.isEmpty()) {
|
||||
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
} else {
|
||||
_state.update { onrampOffersStateFactory.getOffersState(offers) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -245,7 +243,6 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
amountInputManager.query
|
||||
.filter(String::isNotEmpty)
|
||||
.collectLatest { _ ->
|
||||
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
|
||||
startLoadingQuotes()
|
||||
}
|
||||
}
|
||||
|
|
@ -288,74 +285,29 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
|
||||
private fun handleQuoteResult(quotes: List<OnrampQuote>) {
|
||||
sendOnrampQuotesErrorAnalytic(quotes)
|
||||
|
||||
val quote = selectOrUpdateQuote(quotes)
|
||||
|
||||
if (quote == null) {
|
||||
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
lastUpdateState.value = null
|
||||
return
|
||||
}
|
||||
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) }
|
||||
}
|
||||
|
||||
private fun selectOrUpdateQuote(quotes: List<OnrampQuote>): OnrampQuote? {
|
||||
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
|
||||
|
||||
// Check if amount, country or currency has changed
|
||||
val newQuote = if (checkLastInputState(quoteToCheck)) {
|
||||
quoteToCheck
|
||||
if (quotes.all { it is OnrampQuote.AmountError }) {
|
||||
_state.update { amountStateFactory.getAmountSecondaryFieldUpdatedState(quotes) }
|
||||
} else {
|
||||
val state = state.value as? OnrampV2MainComponentUM.Content
|
||||
val providerState = state?.onrampProviderState as? OnrampV2ProvidersUM.Content
|
||||
|
||||
// Get current selected quote to update
|
||||
val lastSelectedQuote = quotes.firstOrNull {
|
||||
it.provider.id == providerState?.providerId &&
|
||||
it.paymentMethod.id == providerState.paymentMethod.id
|
||||
}
|
||||
|
||||
if (lastSelectedQuote is OnrampQuote.Error) {
|
||||
quoteToCheck
|
||||
} else {
|
||||
lastSelectedQuote
|
||||
}
|
||||
_state.update { amountStateFactory.getAmountSecondaryFieldResetState() }
|
||||
}
|
||||
newQuote?.let { updateProvider(newQuote) }
|
||||
|
||||
return newQuote
|
||||
}
|
||||
|
||||
private fun onRetryQuotes() {
|
||||
_state.update {
|
||||
(it as? OnrampV2MainComponentUM.Content)?.copy(
|
||||
errorNotification = null,
|
||||
onrampProviderState = OnrampV2ProvidersUM.Loading,
|
||||
offersBlockState = OnrampOffersBlockUM.Loading(isBlockVisible = false),
|
||||
amountBlockState = it.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading,
|
||||
),
|
||||
offersBlockState = OnrampOffersBlockUM.Loading,
|
||||
amountBlockState = it.amountBlockState.copy(secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty),
|
||||
) ?: it
|
||||
}
|
||||
startLoadingQuotes()
|
||||
}
|
||||
|
||||
private suspend fun updatePairsAndQuotes() {
|
||||
val state = state.value as? OnrampV2MainComponentUM.Content
|
||||
|
||||
if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) {
|
||||
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
|
||||
}
|
||||
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = {
|
||||
_state.update {
|
||||
if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) {
|
||||
return@fold
|
||||
} else {
|
||||
amountStateFactory.getAmountSecondaryResetState()
|
||||
}
|
||||
}
|
||||
_state.update { amountStateFactory.getAmountSecondaryFieldResetState() }
|
||||
},
|
||||
)
|
||||
startLoadingQuotes()
|
||||
|
|
@ -366,18 +318,6 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
_state.update { stateFactory.getOnrampErrorState(onrampError) }
|
||||
}
|
||||
|
||||
private fun updateProvider(quote: OnrampQuote) {
|
||||
lastUpdateState.value = OnrampLastUpdate(
|
||||
fromAmount = quote.fromAmount,
|
||||
countryCode = quote.countryCode,
|
||||
paymentMethod = quote.paymentMethod,
|
||||
)
|
||||
|
||||
_state.update {
|
||||
amountStateFactory.getUpdatedProviderState(selectedQuote = quote)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendOnrampQuotesErrorAnalytic(quotes: List<OnrampQuote>) {
|
||||
quotes.forEach { errorState ->
|
||||
when (errorState) {
|
||||
|
|
@ -398,11 +338,6 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun checkLastInputState(quote: OnrampQuote?): Boolean {
|
||||
return lastUpdateState.value?.fromAmount != quote?.fromAmount ||
|
||||
lastUpdateState.value?.countryCode != quote?.countryCode
|
||||
}
|
||||
|
||||
private fun sendScreenOpenAnalytics() {
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ScreenOpened(
|
||||
|
|
|
|||
|
|
@ -14,16 +14,13 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||
|
||||
|
|
@ -33,14 +30,12 @@ internal fun OnrampFooterContent(
|
|||
boxScope: BoxScope,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
boxScope.apply {
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.align(Alignment.BottomCenter),
|
||||
visible = state.offersBlockState.isBlockVisible.not(),
|
||||
visible = state.offersBlockState is OnrampOffersBlockUM.Empty,
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { it },
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
|
|
@ -55,17 +50,6 @@ internal fun OnrampFooterContent(
|
|||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
text = stringResourceSafe(id = R.string.common_continue),
|
||||
onClick = {
|
||||
state.continueButtonConfig.onClick()
|
||||
keyboardController?.hide()
|
||||
},
|
||||
enabled = state.continueButtonConfig.enabled,
|
||||
)
|
||||
SpacerH(16.dp)
|
||||
OnrampAmountButtons(state = state.onrampAmountButtonUMState)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -43,19 +40,8 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
|
||||
@Composable
|
||||
internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
|
||||
AnimatedVisibility(
|
||||
visible = state.isBlockVisible,
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { it },
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
),
|
||||
exit = slideOutVertically(
|
||||
targetOffsetY = { it },
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
),
|
||||
label = "Offers block animation",
|
||||
) {
|
||||
if (state is OnrampOffersBlockUM.Content) {
|
||||
when (state) {
|
||||
is OnrampOffersBlockUM.Content -> {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
state.recentOffer?.let { recentOffer ->
|
||||
Column {
|
||||
|
|
@ -100,6 +86,15 @@ internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
|
|||
}
|
||||
}
|
||||
}
|
||||
OnrampOffersBlockUM.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LoadingBlock()
|
||||
}
|
||||
}
|
||||
OnrampOffersBlockUM.Empty -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,13 +115,18 @@ internal fun Offer(onrampOfferUM: OnrampOfferUM, modifier: Modifier = Modifier)
|
|||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
OfferHeader(advantage = onrampOfferUM.advantages)
|
||||
RateBlock(rate = onrampOfferUM.rate, diff = onrampOfferUM.diff)
|
||||
RateBlock(
|
||||
rate = onrampOfferUM.rate,
|
||||
diff = onrampOfferUM.diff,
|
||||
isOfferUnavailable = onrampOfferUM.advantages == OnrampOfferAdvantagesUM.Unavailable,
|
||||
)
|
||||
}
|
||||
SpacerWMax()
|
||||
SecondaryButton(
|
||||
PrimaryButton(
|
||||
size = TangemButtonSize.RoundedAction,
|
||||
text = stringResourceSafe(R.string.common_buy),
|
||||
onClick = onrampOfferUM.onBuyClicked,
|
||||
enabled = onrampOfferUM.advantages != OnrampOfferAdvantagesUM.Unavailable,
|
||||
)
|
||||
}
|
||||
SpacerH(10.dp)
|
||||
|
|
@ -142,6 +142,14 @@ internal fun Offer(onrampOfferUM: OnrampOfferUM, modifier: Modifier = Modifier)
|
|||
@Composable
|
||||
private fun OfferHeader(advantage: OnrampOfferAdvantagesUM) {
|
||||
when (advantage) {
|
||||
OnrampOfferAdvantagesUM.GreatRate -> {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.express_provider_great_rate),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
modifier = Modifier.testTag(OnrampOffersBlockTestTags.BEST_RATE_TITLE),
|
||||
)
|
||||
}
|
||||
OnrampOfferAdvantagesUM.Default -> {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onramp_title_you_get),
|
||||
|
|
@ -169,27 +177,24 @@ private fun OfferHeader(advantage: OnrampOfferAdvantagesUM) {
|
|||
}
|
||||
}
|
||||
OnrampOfferAdvantagesUM.Fastest -> {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_fastest_16),
|
||||
tint = TangemTheme.colors.icon.attention,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onramp_offer_type_fastet),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.attention,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onramp_offer_type_fastet),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.attention,
|
||||
)
|
||||
}
|
||||
OnrampOfferAdvantagesUM.Unavailable -> {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onramp_title_available_from),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RateBlock(rate: String, diff: TextReference?) {
|
||||
private fun RateBlock(rate: String, diff: TextReference?, isOfferUnavailable: Boolean) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
|
|
@ -197,7 +202,7 @@ private fun RateBlock(rate: String, diff: TextReference?) {
|
|||
Text(
|
||||
text = rate,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
color = if (isOfferUnavailable) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.testTag(OnrampOffersBlockTestTags.OFFER_TOKEN_AMOUNT),
|
||||
)
|
||||
diff?.let {
|
||||
|
|
@ -314,7 +319,7 @@ internal fun TimingBlock(speed: PaymentMethodType.PaymentSpeed) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun DrawDot(color: Color) {
|
||||
private fun DrawDot(color: Color) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.size(4.dp)
|
||||
|
|
@ -330,6 +335,30 @@ fun DrawDot(color: Color) {
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingBlock(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SpacerH(100.dp)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onramp_fetching_best_rates),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// will be hardcoded till server will be ready to provide this values
|
||||
private const val FEW_MINS_VALUE = "3-5"
|
||||
private const val FEW_DAYS_VALUE = 3
|
||||
|
|
@ -340,7 +369,6 @@ private const val PLENTY_DAYS_VALUE = 5
|
|||
@Composable
|
||||
private fun OnrampOffersContentPreview() {
|
||||
val state = OnrampOffersBlockUM.Content(
|
||||
isBlockVisible = true,
|
||||
recentOffer = OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.RecentlyUsed,
|
||||
advantages = OnrampOfferAdvantagesUM.Default,
|
||||
|
|
@ -350,7 +378,6 @@ private fun OnrampOffersContentPreview() {
|
|||
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||
type = PaymentMethodType.CARD,
|
||||
),
|
||||
providerId = "providerId3",
|
||||
providerName = "Simplex",
|
||||
rate = "0,00045334 BTC",
|
||||
diff = stringReference("–27%"),
|
||||
|
|
@ -359,14 +386,13 @@ private fun OnrampOffersContentPreview() {
|
|||
recommended = persistentListOf(
|
||||
OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = OnrampOfferAdvantagesUM.BestRate,
|
||||
advantages = OnrampOfferAdvantagesUM.GreatRate,
|
||||
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,
|
||||
|
|
@ -381,7 +407,6 @@ private fun OnrampOffersContentPreview() {
|
|||
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%"),
|
||||
|
|
@ -393,4 +418,13 @@ private fun OnrampOffersContentPreview() {
|
|||
TangemThemePreview {
|
||||
OnrampOffersContent(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun OnrampOffersLoadingPreview() {
|
||||
TangemThemePreview {
|
||||
OnrampOffersContent(OnrampOffersBlockUM.Loading)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.features.onramp.mainv2.ui
|
|||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -22,12 +21,10 @@ import androidx.compose.ui.platform.testTag
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
|
@ -36,20 +33,12 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampNewAmountSecondaryFieldUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampNewCurrencyUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampSecondaryFieldErrorUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
val padding = remember(state.offersBlockState.isBlockVisible) {
|
||||
if (state.offersBlockState.isBlockVisible) {
|
||||
22.dp
|
||||
} else {
|
||||
46.dp
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -57,8 +46,8 @@ internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modif
|
|||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
|
||||
)
|
||||
.padding(vertical = padding)
|
||||
.animateContentSize(animationSpec = tween(durationMillis = 300)),
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp)
|
||||
.animateContentSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
OnrampHeaderTitle()
|
||||
|
|
@ -68,8 +57,12 @@ internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modif
|
|||
currencyCode = state.amountBlockState.currencyUM.code,
|
||||
)
|
||||
|
||||
AnimatedVisibility(!state.offersBlockState.isBlockVisible) {
|
||||
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel)
|
||||
AnimatedVisibility(
|
||||
visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty,
|
||||
) {
|
||||
if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) {
|
||||
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
|
@ -130,7 +123,7 @@ private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: Strin
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountSecondary(state: OnrampNewAmountSecondaryFieldUM) {
|
||||
private fun OnrampAmountSecondary(state: OnrampSecondaryFieldErrorUM.Error) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -142,24 +135,12 @@ private fun OnrampAmountSecondary(state: OnrampNewAmountSecondaryFieldUM) {
|
|||
.testTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
when (state) {
|
||||
is OnrampNewAmountSecondaryFieldUM.Content -> Text(
|
||||
text = state.amount.resolveReference(),
|
||||
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
is OnrampNewAmountSecondaryFieldUM.Error -> Text(
|
||||
text = state.error.resolveReference(),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
style = TangemTheme.typography.caption2,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
is OnrampNewAmountSecondaryFieldUM.Loading -> TextShimmer(
|
||||
style = TangemTheme.typography.caption2,
|
||||
modifier = Modifier.width(TangemTheme.dimens.size62),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = state.error.resolveReference(),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
style = TangemTheme.typography.caption2,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue