diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 0f50feba84..0c20fd0993 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -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( diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b9b098fef2..ea88b88529 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -918,7 +918,7 @@ up to %d days %s min - Available from + Available from You get Service is provided by an external provider. \nTangem is not responsible. You can close this screen and check the transaction status on the token details screen. diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampOffer.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampOffer.kt index 56700b01f4..ff2085d060 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampOffer.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampOffer.kt @@ -15,7 +15,7 @@ data class OnrampOffer( ) enum class OnrampOfferAdvantages { - Default, BestRate, Fastest, + Default, BestRate, Fastest, GreatRate, } enum class OnrampOfferCategory { diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt index 17c2e41ae0..7af75375b7 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt @@ -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 diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt index 81ee5e82fd..d6eae3a74f 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt @@ -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, 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 = + 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): 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 && diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampPaymentMethodsUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampPaymentMethodsUseCase.kt index f4528c9e4b..6a8c990b1f 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampPaymentMethodsUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampPaymentMethodsUseCase.kt @@ -19,7 +19,7 @@ class GetOnrampPaymentMethodsUseCase( repository.getAvailablePaymentMethods() .toList() - .sortedBy { it.type.getPriority(isGooglePayAvailable) } + .sortedBy { it.type.getPriorityBySpeed(isGooglePayAvailable) } .toSet() }.mapLeft(errorResolver::resolve) } diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt index 24b27d4860..657c529152 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt @@ -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 } } } diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampV2QuotesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampV2QuotesUseCase.kt deleted file mode 100644 index 5039240734..0000000000 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampV2QuotesUseCase.kt +++ /dev/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>> { - return repository.getQuotes() - .map, Either>> { quotes -> - val isGooglePayAvailable = settingsRepository.isGooglePayAvailability() - quotes.sortedWith( - Comparator.comparing { 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 { - 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()) - } - } - } - } -} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt index 7329a88a84..8d8bb89a05 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/utils/OnrampOfferUtils.kt @@ -1,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 { +/** + * @param isSepaPrioritized Sepa provider should be prioritized over all offers no matter what. + */ +internal fun compareOffersByRateSpeedAndPriority( + isGooglePayAvailable: Boolean, + isSepaPrioritized: Boolean = false, +): Comparator { 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)) } } \ No newline at end of file diff --git a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt index 87f9fbb1c6..663d05542a 100644 --- a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt +++ b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt @@ -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 { 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 { - 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 } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt index 351cd1a5aa..ac5a54cfbd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -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( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt index f60660546a..5f051f3341 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt @@ -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%"), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt index 7c16f78f2e..0547dcbc0e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt @@ -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%"), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt index 54ea1f1451..6603b00623 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt @@ -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, 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 } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt index b151b7818c..9ae8528504 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt @@ -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 { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt index a9f65922cd..7ed9434b91 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt @@ -9,5 +9,4 @@ internal interface OnrampV2Intents { fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM) fun openProviders() fun onRefresh() - fun onContinueClick() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt index 0b6560e380..256aeadd72 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt @@ -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, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt index ec94be047d..34902a7683 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt @@ -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, private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, private val onrampIntents: OnrampV2Intents, - private val cryptoCurrency: CryptoCurrency, ) : Converter { 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, ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt index bb490cc01f..7617651504 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt @@ -17,15 +17,12 @@ internal class OnrampOffersStateFactory( private val onrampIntents: OnrampV2Intents, ) { - fun getOnShowOffersState(offers: List): OnrampV2MainComponentUM { + fun getOffersState(offers: List): 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, - currentState: OnrampV2MainComponentUM.Content, - ): OnrampOffersBlockUM.Content { + private fun mapOnrampOffersBlockToUM(offersBlocks: List): OnrampOffersBlockUM { val allOffersUM = mutableListOf() 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 } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt index 9f08657016..cc17d8bc4e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt @@ -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, 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): 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.AmountError? { + val amountErrorQuotes = quotes.filterIsInstance() + 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 + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt index 631c06be7a..ad9a4c4577 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt @@ -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, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt index 3407fe9696..d7008695dd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt @@ -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() - private val lastUpdateState = mutableStateOf(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 = 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) { 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? { - 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) { 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( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt index 5668fac482..979234f572 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt @@ -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) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt index c39c79a8da..9a1a053cc5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt @@ -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) + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt index b0ea37b98b..5e88e12d6b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt @@ -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, + ) } }