From 4c80e2eaa2f1a48f36a58aab858267052b1f076a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Oct 2025 11:21:17 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../onramp/model/OnrampPaymentMethod.kt | 20 -- .../onramp/model/OnrampPaymentMethodGroup.kt | 12 +- .../onramp/GetOnrampAllOffersUseCase.kt | 157 ++++++-- .../domain/onramp/GetOnrampOffersUseCase.kt | 22 +- .../onramp/GetOnrampAllOffersUseCaseTest.kt | 338 +++++++++++++++++- .../onramp/GetOnrampOffersUseCaseTest.kt | 2 +- .../alloffers/entity/AllOffersStateFactory.kt | 224 +++++++----- .../alloffers/entity/AllOffersStateUM.kt | 2 + .../onramp/alloffers/model/AllOffersModel.kt | 8 +- .../alloffers/ui/AllOffersContentSheet.kt | 3 + .../alloffers/ui/PaymentMethodsContent.kt | 51 ++- .../mainv2/DefaultOnrampV2MainComponent.kt | 5 +- .../factory/OnrampV2AmountStateFactory.kt | 3 +- .../model/OnrampV2MainComponentModel.kt | 18 +- .../ui/OnrampNewMainComponentContent.kt | 3 +- .../paymentmethod/ui/PaymentMethodIcon.kt | 2 +- 16 files changed, 678 insertions(+), 192 deletions(-) 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 7af75375b7..e1049344b2 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,26 +18,6 @@ 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 - REVOLUT_PAY -> 2 - SEPA -> 3 - OTHER -> 4 - } - } else { - when (this) { - CARD -> 2 - REVOLUT_PAY -> 1 - SEPA -> 2 - OTHER -> 3 - GOOGLE_PAY -> 4 - } - } - /** * Get priority regardless of real speed. By business logic. */ diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethodGroup.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethodGroup.kt index 9a957d44ff..68d0fd7c24 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethodGroup.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethodGroup.kt @@ -8,12 +8,10 @@ data class OnrampPaymentMethodGroup( val bestRateOffer: OnrampOffer?, val providerCount: Int, val isBestPaymentMethod: Boolean, -) { + val methodStatus: PaymentMethodStatus, +) - val bestRateAmount: BigDecimal? = bestRateOffer?.let { offer -> - when (val quote = offer.quote) { - is OnrampQuote.Data -> quote.toAmount.value - else -> BigDecimal.ZERO - } - } +sealed interface PaymentMethodStatus { + data object Available : PaymentMethodStatus + data class Unavailable(val availableFrom: BigDecimal) : PaymentMethodStatus } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt index a7e1d2754f..148202162c 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampAllOffersUseCase.kt @@ -5,10 +5,7 @@ import arrow.core.right import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.onramp.model.OnrampOffer -import com.tangem.domain.onramp.model.OnrampOfferAdvantages -import com.tangem.domain.onramp.model.OnrampPaymentMethodGroup -import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.domain.onramp.model.* import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository @@ -35,17 +32,73 @@ class GetOnrampAllOffersUseCase( } private suspend fun processAllOffers(quotes: List): List { - val validQuotes = quotes.filterIsInstance() - if (validQuotes.isEmpty()) return emptyList() + val relevantQuotes = quotes.filter { it !is OnrampQuote.Error } + if (relevantQuotes.isEmpty()) return emptyList() + val isGooglePayAvailable = settingsRepository.isGooglePayAvailability() - val overallBestRateQuote = validQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable)) + val validQuotes = relevantQuotes.filterIsInstance() + val overallBestRateQuote = findOverallBestRateQuote(validQuotes, isGooglePayAvailable) val bestRate = overallBestRateQuote?.toAmount?.value - val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod } + val offersByPaymentMethod = relevantQuotes.groupBy { it.paymentMethod } - return offersByPaymentMethod.map { (paymentMethod, methodQuotes) -> - val methodOffers = methodQuotes.map { quote -> + return offersByPaymentMethod + .map { (paymentMethod, methodQuotes) -> + createPaymentMethodGroup( + paymentMethod = paymentMethod, + methodQuotes = methodQuotes, + overallBestRateQuote = overallBestRateQuote, + bestRate = bestRate, + isGooglePayAvailable = isGooglePayAvailable, + ) + } + .sortedBy { it.paymentMethod.type.getPriorityForMethod(isGooglePayAvailable) } + } + + private fun findOverallBestRateQuote( + validQuotes: List, + isGooglePayAvailable: Boolean, + ): OnrampQuote.Data? { + return validQuotes.maxWithOrNull( + compareOffersByRateSpeedAndPriority( + isGooglePayAvailable = isGooglePayAvailable, + isSepaPrioritized = false, + ), + ) + } + + private fun createPaymentMethodGroup( + paymentMethod: OnrampPaymentMethod, + methodQuotes: List, + overallBestRateQuote: OnrampQuote.Data?, + bestRate: BigDecimal?, + isGooglePayAvailable: Boolean, + ): OnrampPaymentMethodGroup { + val methodOffers = methodQuotes.mapNotNull { quote -> + createOffer(quote, overallBestRateQuote, bestRate) + } + + val groupBestRateQuote = findGroupBestRateQuote(methodQuotes, isGooglePayAvailable) + val groupBestRateOffer = findBestRateOffer(methodOffers, groupBestRateQuote) + + return OnrampPaymentMethodGroup( + paymentMethod = paymentMethod, + offers = sortOffers(methodOffers), + providerCount = countUniqueProviders(methodOffers), + bestRateOffer = groupBestRateOffer, + isBestPaymentMethod = overallBestRateQuote?.paymentMethod == paymentMethod, + methodStatus = determineMethodStatus(methodQuotes), + ) + } + + private fun createOffer( + quote: OnrampQuote, + overallBestRateQuote: OnrampQuote.Data?, + bestRate: BigDecimal?, + ): OnrampOffer? { + return when (quote) { + is OnrampQuote.Data -> { val advantages = if (quote == overallBestRateQuote) { OnrampOfferAdvantages.BestRate } else { @@ -54,28 +107,70 @@ class GetOnrampAllOffersUseCase( val rateDif = calculateRateDif(quote.toAmount.value, bestRate) OnrampOffer(quote = quote, rateDif = rateDif, advantages = advantages) } - - val groupBestRateOfferData = - methodQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable)) - val groupBestRateOffer = methodOffers.find { - when (val quote = it.quote) { - is OnrampQuote.Data -> quote == groupBestRateOfferData - else -> false - } + is OnrampQuote.AmountError -> { + OnrampOffer(quote = quote, rateDif = null, advantages = OnrampOfferAdvantages.Default) } + is OnrampQuote.Error -> null + } + } - OnrampPaymentMethodGroup( - paymentMethod = paymentMethod, - offers = methodOffers.sortedByDescending { offer -> - when (val quote = offer.quote) { - is OnrampQuote.Data -> quote.toAmount.value - else -> BigDecimal.ZERO - } - }, - providerCount = methodOffers.map { it.quote.provider.id }.distinct().size, - bestRateOffer = groupBestRateOffer, - isBestPaymentMethod = overallBestRateQuote?.paymentMethod == paymentMethod, - ) - }.sortedBy { it.paymentMethod.type.getPriority(isGooglePayAvailable) } + private fun findGroupBestRateQuote( + validMethodQuotes: List, + isGooglePayAvailable: Boolean, + ): OnrampQuote? { + val dataQuotes = validMethodQuotes.filterIsInstance() + val amountErrorQuotes = validMethodQuotes.filterIsInstance() + + return when { + dataQuotes.isNotEmpty() -> { + dataQuotes.maxWithOrNull( + compareOffersByRateSpeedAndPriority( + isGooglePayAvailable = isGooglePayAvailable, + isSepaPrioritized = false, + ), + ) + } + amountErrorQuotes.isNotEmpty() -> { + amountErrorQuotes.minByOrNull { it.error.requiredAmount } + } + else -> validMethodQuotes.firstOrNull() + } + } + + private fun findBestRateOffer(methodOffers: List, groupBestRateQuote: OnrampQuote?): OnrampOffer? { + if (groupBestRateQuote == null) return null + + return methodOffers.find { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> quote == groupBestRateQuote + is OnrampQuote.AmountError -> quote == groupBestRateQuote + is OnrampQuote.Error -> false + } + } + } + + private fun sortOffers(methodOffers: List): List { + return methodOffers.sortedByDescending { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> quote.toAmount.value + else -> BigDecimal.ZERO + } + } + } + + private fun countUniqueProviders(methodOffers: List): Int { + return methodOffers.map { it.quote.provider.id }.distinct().size + } + + private fun determineMethodStatus(methodQuotes: List): PaymentMethodStatus { + val hasAtLeastOneValidQuote = methodQuotes.any { it is OnrampQuote.Data } + return if (hasAtLeastOneValidQuote) { + PaymentMethodStatus.Available + } else { + val minSum = methodQuotes + .filterIsInstance() + .minOfOrNull { it.error.requiredAmount } ?: BigDecimal.ZERO + PaymentMethodStatus.Unavailable(minSum) + } } } \ No newline at end of file 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 d6eae3a74f..f6ea2fd0c3 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 @@ -77,8 +77,7 @@ class GetOnrampOffersUseCase( return offers.find { offer -> offer.quote.provider.info.name == lastTransaction.providerName && - offer.quote.paymentMethod.name == lastTransaction.paymentMethod && - lastTransaction.status.isTerminal + offer.quote.paymentMethod.name == lastTransaction.paymentMethod && isRecentUsed(lastTransaction.status) } } @@ -249,4 +248,23 @@ class GetOnrampOffersUseCase( return offer1.quote.provider.id == offer2.quote.provider.id && offer1.quote.paymentMethod.id == offer2.quote.paymentMethod.id } + + private fun isRecentUsed(onrampStatus: OnrampStatus.Status): Boolean { + return when (onrampStatus) { + OnrampStatus.Status.Created, + OnrampStatus.Status.Expired, + OnrampStatus.Status.Paused, + OnrampStatus.Status.WaitingForPayment, + OnrampStatus.Status.PaymentProcessing, + OnrampStatus.Status.Verifying, + OnrampStatus.Status.Paid, + OnrampStatus.Status.Sending, + OnrampStatus.Status.RefundInProgress, + -> false + OnrampStatus.Status.Failed, + OnrampStatus.Status.Finished, + OnrampStatus.Status.Refunded, + -> true + } + } } \ No newline at end of file diff --git a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampAllOffersUseCaseTest.kt b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampAllOffersUseCaseTest.kt index 89c4b8e6f9..6158e843b4 100644 --- a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampAllOffersUseCaseTest.kt +++ b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampAllOffersUseCaseTest.kt @@ -3,10 +3,14 @@ package com.tangem.domain.onramp import com.google.common.truth.Truth import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.onramp.model.OnrampAmount import com.tangem.domain.onramp.model.OnrampOfferAdvantages import com.tangem.domain.onramp.model.OnrampPaymentMethod import com.tangem.domain.onramp.model.OnrampProvider import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.domain.onramp.model.PaymentMethodStatus +import com.tangem.domain.onramp.model.PaymentMethodType +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 @@ -40,7 +44,7 @@ class GetOnrampAllOffersUseCaseTest { } @Test - fun `invoke should return empty list when no valid quotes`() = runTest { + fun `should return empty list when no quotes`() = runTest { val emptyQuotes = listOf() coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes) @@ -57,16 +61,40 @@ class GetOnrampAllOffersUseCaseTest { } @Test - fun `invoke should return grouped offers with best rate marked`() = runTest { - val paymentMethod1 = createMockPaymentMethod("card", "Card") - val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer") + fun `should return empty list when only Error quotes`() = runTest { + val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD) + val provider = createMockProvider("provider1", "Provider 1") + + val quotes = listOf( + createMockErrorQuote(paymentMethod, provider), + createMockErrorQuote(paymentMethod, provider), + ) + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { settingsRepository.isGooglePayAvailability() } returns false + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> Truth.assertThat(offers).isEmpty() }, + ) + } + } + + @Test + fun `should group offers by payment method with best rate marked`() = runTest { + 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") val quotes = listOf( - createMockQuote(paymentMethod1, provider1, BigDecimal("100.0")), - createMockQuote(paymentMethod1, provider2, BigDecimal("95.0")), - createMockQuote(paymentMethod2, provider1, BigDecimal("98.0")), + createMockDataQuote(paymentMethod1, provider1, BigDecimal("100.0")), + createMockDataQuote(paymentMethod1, provider2, BigDecimal("95.0")), + createMockDataQuote(paymentMethod2, provider1, BigDecimal("98.0")), ) coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) @@ -86,6 +114,7 @@ class GetOnrampAllOffersUseCaseTest { Truth.assertThat(cardGroup?.offers).hasSize(2) Truth.assertThat(cardGroup?.providerCount).isEqualTo(2) Truth.assertThat(cardGroup?.isBestPaymentMethod).isTrue() + Truth.assertThat(cardGroup?.methodStatus).isEqualTo(PaymentMethodStatus.Available) val bestRateOffer = cardGroup?.offers?.find { it.advantages == OnrampOfferAdvantages.BestRate } Truth.assertThat(bestRateOffer).isNotNull() @@ -95,6 +124,7 @@ class GetOnrampAllOffersUseCaseTest { Truth.assertThat(bankGroup?.offers).hasSize(1) Truth.assertThat(bankGroup?.providerCount).isEqualTo(1) Truth.assertThat(bankGroup?.isBestPaymentMethod).isFalse() + Truth.assertThat(bankGroup?.methodStatus).isEqualTo(PaymentMethodStatus.Available) }, ) } @@ -104,14 +134,14 @@ class GetOnrampAllOffersUseCaseTest { } @Test - fun `invoke should sort offers by toAmount descending`() = runTest { - val paymentMethod = createMockPaymentMethod("card", "Card") + fun `should sort offers by toAmount descending`() = runTest { + val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD) val provider = createMockProvider("provider1", "Provider 1") val quotes = listOf( - createMockQuote(paymentMethod, provider, BigDecimal("90.0")), - createMockQuote(paymentMethod, provider, BigDecimal("100.0")), - createMockQuote(paymentMethod, provider, BigDecimal("95.0")), + createMockDataQuote(paymentMethod, provider, BigDecimal("90.0")), + createMockDataQuote(paymentMethod, provider, BigDecimal("100.0")), + createMockDataQuote(paymentMethod, provider, BigDecimal("95.0")), ) coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) @@ -144,13 +174,248 @@ class GetOnrampAllOffersUseCaseTest { } } - private fun createMockPaymentMethod(id: String, name: String): OnrampPaymentMethod { + @Test + fun `should include AmountError offers in group`() = runTest { + val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD) + val provider1 = createMockProvider("provider1", "Provider 1") + val provider2 = createMockProvider("provider2", "Provider 2") + + val quotes = listOf( + createMockDataQuote(paymentMethod, provider1, BigDecimal("100.0")), + createMockAmountErrorQuote(paymentMethod, provider2), + ) + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { settingsRepository.isGooglePayAvailability() } returns false + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val cardGroup = offers.first() + Truth.assertThat(cardGroup.offers).hasSize(2) + Truth.assertThat(cardGroup.methodStatus).isEqualTo(PaymentMethodStatus.Available) + + val dataOffer = cardGroup.offers.find { it.quote is OnrampQuote.Data } + Truth.assertThat(dataOffer).isNotNull() + Truth.assertThat(dataOffer?.rateDif).isNull() // Best rate offer has null rateDif + + val amountErrorOffer = cardGroup.offers.find { it.quote is OnrampQuote.AmountError } + Truth.assertThat(amountErrorOffer).isNotNull() + Truth.assertThat(amountErrorOffer?.rateDif).isNull() + Truth.assertThat(amountErrorOffer?.advantages).isEqualTo(OnrampOfferAdvantages.Default) + }, + ) + } + } + + @Test + fun `should mark payment method as Unavailable when only AmountError offers`() = runTest { + val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD) + val provider1 = createMockProvider("provider1", "Provider 1") + val provider2 = createMockProvider("provider2", "Provider 2") + + val quotes = listOf( + createMockAmountErrorQuote(paymentMethod, provider1, requiredAmount = BigDecimal("100.0")), + createMockAmountErrorQuote(paymentMethod, provider2, requiredAmount = BigDecimal("50.0")), + ) + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { settingsRepository.isGooglePayAvailability() } returns false + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val cardGroup = offers.first() + Truth.assertThat( + cardGroup.methodStatus, + ).isEqualTo(PaymentMethodStatus.Unavailable(BigDecimal("50.0"))) + Truth.assertThat(cardGroup.offers).hasSize(2) + + Truth.assertThat(cardGroup.bestRateOffer).isNotNull() + val bestQuote = cardGroup.bestRateOffer?.quote as? OnrampQuote.AmountError + Truth.assertThat(bestQuote?.error?.requiredAmount).isEqualTo(BigDecimal("50.0")) + + Truth.assertThat(cardGroup.isBestPaymentMethod).isFalse() + + cardGroup.offers.forEach { offer -> + Truth.assertThat(offer.quote).isInstanceOf(OnrampQuote.AmountError::class.java) + Truth.assertThat(offer.rateDif).isNull() + } + }, + ) + } + } + + @Test + fun `should not include Error quotes in groups`() = runTest { + val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD) + val provider1 = createMockProvider("provider1", "Provider 1") + val provider2 = createMockProvider("provider2", "Provider 2") + + val quotes = listOf( + createMockDataQuote(paymentMethod, provider1, BigDecimal("100.0")), + createMockErrorQuote(paymentMethod, provider2), + ) + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { settingsRepository.isGooglePayAvailability() } returns false + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val cardGroup = offers.first() + Truth.assertThat(cardGroup.offers).hasSize(1) + Truth.assertThat(cardGroup.offers.first().quote).isInstanceOf(OnrampQuote.Data::class.java) + Truth.assertThat(cardGroup.methodStatus).isEqualTo(PaymentMethodStatus.Available) + }, + ) + } + } + + @Test + fun `should prioritize Data over AmountError for best rate`() = runTest { + val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD) + val provider1 = createMockProvider("provider1", "Provider 1") + val provider2 = createMockProvider("provider2", "Provider 2") + val provider3 = createMockProvider("provider3", "Provider 3") + + val quotes = listOf( + createMockDataQuote(paymentMethod, provider1, BigDecimal("90.0")), + createMockAmountErrorQuote(paymentMethod, provider2, requiredAmount = BigDecimal("10.0")), + createMockDataQuote(paymentMethod, provider3, BigDecimal("100.0")), + ) + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { settingsRepository.isGooglePayAvailability() } returns false + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val cardGroup = offers.first() + Truth.assertThat(cardGroup.offers).hasSize(3) + Truth.assertThat(cardGroup.methodStatus).isEqualTo(PaymentMethodStatus.Available) + + Truth.assertThat(cardGroup.bestRateOffer).isNotNull() + val bestQuote = cardGroup.bestRateOffer?.quote as? OnrampQuote.Data + Truth.assertThat(bestQuote?.toAmount?.value).isEqualTo(BigDecimal("100.0")) + }, + ) + } + } + + @Test + fun `should sort AmountError offers to bottom`() = runTest { + val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD) + val provider1 = createMockProvider("provider1", "Provider 1") + val provider2 = createMockProvider("provider2", "Provider 2") + val provider3 = createMockProvider("provider3", "Provider 3") + + val quotes = listOf( + createMockAmountErrorQuote(paymentMethod, provider1, requiredAmount = BigDecimal("50.0")), + createMockDataQuote(paymentMethod, provider2, BigDecimal("95.0")), + createMockDataQuote(paymentMethod, provider3, BigDecimal("100.0")), + ) + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { settingsRepository.isGooglePayAvailability() } returns false + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val cardGroup = offers.first() + Truth.assertThat(cardGroup.offers).hasSize(3) + + Truth.assertThat(cardGroup.offers[0].quote).isInstanceOf(OnrampQuote.Data::class.java) + Truth.assertThat(cardGroup.offers[1].quote).isInstanceOf(OnrampQuote.Data::class.java) + Truth.assertThat(cardGroup.offers[2].quote).isInstanceOf(OnrampQuote.AmountError::class.java) + + val firstAmount = (cardGroup.offers[0].quote as OnrampQuote.Data).toAmount.value + val secondAmount = (cardGroup.offers[1].quote as OnrampQuote.Data).toAmount.value + Truth.assertThat(firstAmount).isEqualTo(BigDecimal("100.0")) + Truth.assertThat(secondAmount).isEqualTo(BigDecimal("95.0")) + }, + ) + } + } + + @Test + fun `should handle mixed Data AmountError and Error quotes correctly`() = runTest { + val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD) + val provider1 = createMockProvider("provider1", "Provider 1") + val provider2 = createMockProvider("provider2", "Provider 2") + val provider3 = createMockProvider("provider3", "Provider 3") + val provider4 = createMockProvider("provider4", "Provider 4") + + val quotes = listOf( + createMockDataQuote(paymentMethod, provider1, BigDecimal("100.0")), + createMockAmountErrorQuote(paymentMethod, provider2, requiredAmount = BigDecimal("50.0")), + createMockErrorQuote(paymentMethod, provider3), + createMockDataQuote(paymentMethod, provider4, BigDecimal("95.0")), + ) + + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { settingsRepository.isGooglePayAvailability() } returns false + + val result = useCase(userWalletId, cryptoCurrencyId) + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val cardGroup = offers.first() + Truth.assertThat(cardGroup.offers).hasSize(3) + Truth.assertThat(cardGroup.methodStatus).isEqualTo(PaymentMethodStatus.Available) + + val dataOffers = cardGroup.offers.filter { it.quote is OnrampQuote.Data } + val amountErrorOffers = cardGroup.offers.filter { it.quote is OnrampQuote.AmountError } + val errorOffers = cardGroup.offers.filter { it.quote is OnrampQuote.Error } + + Truth.assertThat(dataOffers).hasSize(2) + Truth.assertThat(amountErrorOffers).hasSize(1) + Truth.assertThat(errorOffers).isEmpty() + }, + ) + } + } + + private fun createMockPaymentMethod(id: String, name: String, type: PaymentMethodType): OnrampPaymentMethod { return mockk { every { this@mockk.id } returns id every { this@mockk.name } returns name - every { this@mockk.type } returns mockk { - every { getPriority(any()) } returns 1 - } + every { this@mockk.type } returns type } } @@ -161,7 +426,7 @@ class GetOnrampAllOffersUseCaseTest { } } - private fun createMockQuote( + private fun createMockDataQuote( paymentMethod: OnrampPaymentMethod, provider: OnrampProvider, toAmount: BigDecimal, @@ -169,9 +434,44 @@ class GetOnrampAllOffersUseCaseTest { return mockk { every { this@mockk.paymentMethod } returns paymentMethod every { this@mockk.provider } returns provider - every { this@mockk.toAmount } returns mockk { - every { value } returns toAmount + every { this@mockk.toAmount } returns createMockAmount(toAmount) + every { this@mockk.fromAmount } returns createMockAmount(BigDecimal("10.0")) + every { this@mockk.countryCode } returns "US" + every { this@mockk.minFromAmount } returns null + every { this@mockk.maxFromAmount } returns null + } + } + + private fun createMockAmountErrorQuote( + paymentMethod: OnrampPaymentMethod, + provider: OnrampProvider, + requiredAmount: BigDecimal = BigDecimal("50.0"), + ): OnrampQuote.AmountError { + return mockk { + every { this@mockk.paymentMethod } returns paymentMethod + every { this@mockk.provider } returns provider + every { this@mockk.fromAmount } returns createMockAmount(BigDecimal("10.0")) + every { this@mockk.countryCode } returns "US" + every { this@mockk.error } returns mockk { + every { this@mockk.requiredAmount } returns requiredAmount } } } + + private fun createMockErrorQuote(paymentMethod: OnrampPaymentMethod, provider: OnrampProvider): OnrampQuote.Error { + return mockk { + every { this@mockk.paymentMethod } returns paymentMethod + every { this@mockk.provider } returns provider + every { this@mockk.fromAmount } returns createMockAmount(BigDecimal("10.0")) + every { this@mockk.countryCode } returns "US" + every { this@mockk.error } returns mockk() + } + } + + private fun createMockAmount(value: BigDecimal): OnrampAmount { + return mockk { + every { this@mockk.value } returns value + every { this@mockk.symbol } returns "USD" + } + } } \ 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 663d05542a..743ca16aae 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 @@ -285,7 +285,7 @@ class GetOnrampOffersUseCaseTest { 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 + every { this@mockk.status } returns OnrampStatus.Status.Finished } } } \ 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 ac5a54cfbd..6692632302 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 @@ -4,6 +4,7 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.stringReference 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.core.ui.format.bigdecimal.percent import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent @@ -14,6 +15,7 @@ import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.MINUS +import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal @@ -23,42 +25,64 @@ internal class AllOffersStateFactory( private val allOffersIntents: AllOffersIntents, ) { - fun getLoadedPaymentsState(methodGroups: List): AllOffersStateUM { + fun getLoadedPaymentsState(methodGroups: List, currencyCode: String): AllOffersStateUM { return AllOffersStateUM.Content( - methods = methodGroups.map { methodGroup -> - AllOffersPaymentMethodUM( - offers = mapOffersToUM(methodGroup.offers).toPersistentList(), - methodConfig = OnrampPaymentMethodConfig( - method = methodGroup.paymentMethod, - onClick = { allOffersIntents.onPaymentMethodClicked(methodGroup.paymentMethod.id) }, - ), - diff = methodGroup - .bestRateOffer - ?.rateDif - ?.takeIf { it > BigDecimal.ZERO } - ?.let { diff -> - stringReference("$MINUS${diff.format { percent() }}") - }, - rate = methodGroup.bestRateOffer?.let { offer -> - when (val quote = offer.quote) { - is OnrampQuote.Data -> quote.toAmount.value.format { - crypto( - symbol = quote.toAmount.symbol, - decimals = quote.toAmount.decimals, - ) - } - else -> "" - } - } ?: "", - providersCount = methodGroup.providerCount, - isBestRate = methodGroup.isBestPaymentMethod, - ) + methods = methodGroups.map { group -> + createPaymentMethodUM(group, currencyCode) }.toPersistentList(), - currentMethod = null, + currentMethod = replaceOffersForCurrentMethod(methodGroups, currencyCode), onBackClicked = { allOffersIntents.onBackClicked() }, ) } + private fun createPaymentMethodUM( + methodGroup: OnrampPaymentMethodGroup, + currencyCode: String, + ): AllOffersPaymentMethodUM { + return AllOffersPaymentMethodUM( + offers = mapOffersToUM(methodGroup.offers, currencyCode).toPersistentList(), + methodConfig = createMethodConfig(methodGroup.paymentMethod), + diff = formatRateDiff(methodGroup.bestRateOffer?.rateDif), + rate = formatBestRate(methodGroup.bestRateOffer, currencyCode), + providersCount = methodGroup.providerCount, + isBestRate = methodGroup.isBestPaymentMethod, + paymentMethodStatus = methodGroup.methodStatus, + ) + } + + private fun createMethodConfig(paymentMethod: OnrampPaymentMethod): OnrampPaymentMethodConfig { + return OnrampPaymentMethodConfig( + method = paymentMethod, + onClick = { allOffersIntents.onPaymentMethodClicked(paymentMethod.id) }, + ) + } + + private fun formatRateDiff(rateDif: BigDecimal?) = rateDif + ?.takeIf { it > BigDecimal.ZERO } + ?.let { diff -> stringReference("$MINUS${diff.format { percent() }}") } + + private fun formatBestRate(bestRateOffer: OnrampOffer?, currencyCode: String): String { + return when (val quote = bestRateOffer?.quote) { + is OnrampQuote.Data -> formatCryptoAmount(quote.toAmount) + is OnrampQuote.AmountError -> formatRequiredAmount(quote, currencyCode) + is OnrampQuote.Error, + null, + -> "" + } + } + + private fun formatCryptoAmount(amount: OnrampAmount): String { + return amount.value.format { + crypto(symbol = amount.symbol, decimals = amount.decimals) + } + } + + private fun formatRequiredAmount(quote: OnrampQuote.AmountError, currencyCode: String): String { + return quote.error.requiredAmount.format { + fiat(fiatCurrencySymbol = quote.fromAmount.symbol, fiatCurrencyCode = currencyCode) + } + } + fun getPaymentsState(): AllOffersStateUM { return when (val currentState = currentStateProvider.invoke()) { is AllOffersStateUM.Content -> { @@ -72,34 +96,36 @@ internal class AllOffersStateFactory( } fun getOnrampErrorState(onrampError: OnrampError): AllOffersStateUM { - return when (onrampError) { - is OnrampError.DataError -> getErrorState( - errorCode = onrampError.code, - onRefresh = allOffersIntents::onRefresh, - ) - OnrampError.PairsNotFound, - is OnrampError.DomainError, - -> getErrorState(onRefresh = allOffersIntents::onRefresh) - is OnrampError.AmountError.TooBigError, - is OnrampError.AmountError.TooSmallError, - OnrampError.RedirectError.VerificationFailed, - OnrampError.RedirectError.WrongRequestId, - -> currentStateProvider() + return if (shouldShowErrorState(onrampError)) { + createErrorState(errorCode = (onrampError as? OnrampError.DataError)?.code) + } else { + currentStateProvider() } } - private fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): AllOffersStateUM { - val state = currentStateProvider() - return when (state) { + private fun shouldShowErrorState(error: OnrampError): Boolean { + return when (error) { + is OnrampError.DataError, + OnrampError.PairsNotFound, + is OnrampError.DomainError, + -> true + is OnrampError.AmountError, + is OnrampError.RedirectError, + -> false + } + } + + private fun createErrorState(errorCode: String? = null): AllOffersStateUM { + return when (currentStateProvider()) { is AllOffersStateUM.Content, AllOffersStateUM.Loading, -> AllOffersStateUM.Error( errorNotification = NotificationUM.Warning.OnrampErrorNotification( errorCode = errorCode, - onRefresh = onRefresh, + onRefresh = allOffersIntents::onRefresh, ), ) - is AllOffersStateUM.Error -> state + is AllOffersStateUM.Error -> currentStateProvider() } } @@ -108,52 +134,70 @@ internal class AllOffersStateFactory( OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest - OnrampOfferAdvantages.GreatRate -> OnrampOfferAdvantagesUM.Default + OnrampOfferAdvantages.GreatRate -> OnrampOfferAdvantagesUM.GreatRate } } - private fun mapOffersToUM(offers: List): List { - return buildList { - offers.forEach { offer -> - when (val quote = offer.quote) { - is OnrampQuote.Data -> { - add( - OnrampOfferUM( - category = OnrampOfferCategoryUM.Recommended, - advantages = mapOfferAdvantagesDTOtoUM(offer.advantages), - paymentMethod = quote.paymentMethod, - providerName = quote.provider.info.name, - rate = quote.toAmount.value.format { - crypto( - symbol = quote.toAmount.symbol, - decimals = quote.toAmount.decimals, - ) - }, - diff = offer - .rateDif - ?.takeIf { it > BigDecimal.ZERO } - ?.let { diff -> - stringReference("$MINUS${diff.format { percent() }}") - }, - onBuyClicked = { - allOffersIntents.onBuyClick( - quote = OnrampProviderWithQuote.Data( - provider = quote.provider, - paymentMethod = quote.paymentMethod, - toAmount = quote.toAmount, - fromAmount = quote.fromAmount, - ), - onrampOfferAdvantagesUM = mapOfferAdvantagesDTOtoUM(offer.advantages), - ) - }, - ), - ) - } - is OnrampQuote.AmountError, - is OnrampQuote.Error, - -> Unit - } + private fun mapOffersToUM(offers: List, currencyCode: String): List { + return offers.mapNotNull { offer -> + when (val quote = offer.quote) { + is OnrampQuote.Data -> createDataOfferUM(quote, offer) + is OnrampQuote.AmountError -> createAmountErrorOfferUM(quote, offer, currencyCode) + is OnrampQuote.Error -> null } } } + + private fun createDataOfferUM(quote: OnrampQuote.Data, offer: OnrampOffer): OnrampOfferUM { + return OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = mapOfferAdvantagesDTOtoUM(offer.advantages), + paymentMethod = quote.paymentMethod, + providerName = quote.provider.info.name, + rate = formatCryptoAmount(quote.toAmount), + diff = formatRateDiff(offer.rateDif), + onBuyClicked = { + allOffersIntents.onBuyClick( + quote = OnrampProviderWithQuote.Data( + provider = quote.provider, + paymentMethod = quote.paymentMethod, + toAmount = quote.toAmount, + fromAmount = quote.fromAmount, + ), + onrampOfferAdvantagesUM = mapOfferAdvantagesDTOtoUM(offer.advantages), + ) + }, + ) + } + + private fun createAmountErrorOfferUM( + quote: OnrampQuote.AmountError, + offer: OnrampOffer, + currencyCode: String, + ): OnrampOfferUM { + return OnrampOfferUM( + category = OnrampOfferCategoryUM.Recommended, + advantages = OnrampOfferAdvantagesUM.Unavailable, + paymentMethod = quote.paymentMethod, + providerName = quote.provider.info.name, + rate = formatRequiredAmount(quote, currencyCode), + diff = formatRateDiff(offer.rateDif), + onBuyClicked = {}, + ) + } + + private fun replaceOffersForCurrentMethod( + methodGroups: List, + currencyCode: String, + ): AllOffersPaymentMethodUM? { + val currentMethod = (currentStateProvider() as? AllOffersStateUM.Content)?.currentMethod ?: return null + + val updateForCurrentMethod = methodGroups.find { it.paymentMethod.id == currentMethod.methodConfig.method.id } + + return updateForCurrentMethod?.let { updatedCurrentMethod -> + currentMethod.copy( + offers = mapOffersToUM(updatedCurrentMethod.offers, currencyCode).toImmutableList(), + ) + } + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt index 834bc396b3..8e1847afc5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.alloffers.entity import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.PaymentMethodStatus import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM import kotlinx.collections.immutable.ImmutableList @@ -26,6 +27,7 @@ internal data class AllOffersPaymentMethodUM( val rate: String, val providersCount: Int, val isBestRate: Boolean, + val paymentMethodStatus: PaymentMethodStatus, ) internal data class OnrampPaymentMethodConfig( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index 29414b13c0..12b43915ad 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -74,6 +74,7 @@ internal class AllOffersModel @Inject constructor( providerName = quote.provider.info.name, paymentMethodName = quote.paymentMethod.name, )?.let { analyticsEventHandler::send } + dismiss() params.openRedirectPage(quote) } @@ -95,7 +96,12 @@ internal class AllOffersModel @Inject constructor( maybeOffers.fold( ifLeft = ::handleOnrampError, ifRight = { offersGroup -> - _state.update { stateFactory.getLoadedPaymentsState(offersGroup) } + _state.update { + stateFactory.getLoadedPaymentsState( + methodGroups = offersGroup, + currencyCode = params.amountCurrencyCode, + ) + } }, ) } 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 5f051f3341..e59a517b99 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 @@ -26,6 +26,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.PaymentMethodStatus import com.tangem.domain.onramp.model.PaymentMethodType import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM @@ -237,6 +238,7 @@ private fun AllOffersContentSheetPaymentPreview() { rate = "0,0245334 BTC", providersCount = 2, isBestRate = true, + paymentMethodStatus = PaymentMethodStatus.Available, ) TangemThemePreview { @@ -302,6 +304,7 @@ private fun AllOffersContentSheetOffersPreview() { rate = "0,0245334 BTC", providersCount = 2, isBestRate = true, + paymentMethodStatus = PaymentMethodStatus.Available, ) } 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 0547dcbc0e..928989b690 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 @@ -28,6 +28,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.SelectPaymentMethodBottomSheetTestTags import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.PaymentMethodStatus import com.tangem.domain.onramp.model.PaymentMethodType import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig @@ -89,21 +90,50 @@ private fun PaymentMethod(methodUM: AllOffersPaymentMethodUM, modifier: Modifier SpacerW(12.dp) Column { - PaymentMethodInfoBlock( - paymentMethodName = methodUM.methodConfig.method.name, - rate = methodUM.rate, - diff = methodUM.diff, - isBestRate = methodUM.isBestRate, - ) - Row(verticalAlignment = Alignment.CenterVertically) { - ProvidersCountBlockInfo(providersCount = methodUM.providersCount) - SpacerW(8.dp) - TimingBlockInfo(speed = methodUM.methodConfig.method.type.getProcessingSpeed()) + when (methodUM.paymentMethodStatus) { + PaymentMethodStatus.Available -> { + PaymentMethodInfoBlock( + paymentMethodName = methodUM.methodConfig.method.name, + rate = methodUM.rate, + diff = methodUM.diff, + isBestRate = methodUM.isBestRate, + ) + Row(verticalAlignment = Alignment.CenterVertically) { + ProvidersCountBlockInfo(providersCount = methodUM.providersCount) + SpacerW(8.dp) + TimingBlockInfo(speed = methodUM.methodConfig.method.type.getProcessingSpeed()) + } + } + is PaymentMethodStatus.Unavailable -> { + UnavailablePaymentMethodInfoBlock( + paymentMethodName = methodUM.methodConfig.method.name, + errorAmount = methodUM.rate, + ) + } } } } } +@Composable +private fun UnavailablePaymentMethodInfoBlock(paymentMethodName: String, errorAmount: String) { + Column(modifier = Modifier.padding(bottom = 14.dp)) { + Text( + text = paymentMethodName, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_NAME), + ) + SpacerH(2.dp) + Text( + text = stringResourceSafe(R.string.onramp_provider_min_amount, errorAmount), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.UP_TO_TEXT), + ) + } +} + @Composable private fun PaymentMethodInfoBlock( paymentMethodName: String, @@ -267,6 +297,7 @@ private fun PaymentMethodsContentPreview() { rate = "0,0245334 BTC", providersCount = 2, isBestRate = true, + paymentMethodStatus = PaymentMethodStatus.Available, ) TangemThemePreview { PaymentMethodsContent(persistentListOf(method)) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt index fe65d540a7..fa206dc5c1 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt @@ -67,7 +67,10 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor( cryptoCurrency = params.cryptoCurrency, country = config.country, launchSepa = false, - onDismiss = { model.bottomSheetNavigation.dismiss() }, + onDismiss = { + model.bottomSheetNavigation.dismiss() + model.handleOnrampAvailable() + }, ), ) is OnrampV2MainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create( 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 cc17d8bc4e..2af5055bd7 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 @@ -67,7 +67,7 @@ internal class OnrampV2AmountStateFactory( ) } - fun getAmountSecondaryFieldUpdatedState(quotes: List): OnrampV2MainComponentUM { + fun getSecondaryFieldAmountErrorState(quotes: List): OnrampV2MainComponentUM { val currentState = currentStateProvider() if (currentState !is OnrampV2MainComponentUM.Content) return currentState @@ -82,6 +82,7 @@ internal class OnrampV2AmountStateFactory( ?: OnrampSecondaryFieldErrorUM.Empty, ), errorNotification = null, + offersBlockState = OnrampOffersBlockUM.Empty, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt index d7008695dd..1f36fd34ca 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 @@ -229,9 +229,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( maybeOffers.fold( ifLeft = ::handleOnrampError, ifRight = { offers -> - if (offers.isEmpty()) { - _state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } - } else { + if (offers.isNotEmpty()) { _state.update { onrampOffersStateFactory.getOffersState(offers) } } }, @@ -285,10 +283,16 @@ internal class OnrampV2MainComponentModel @Inject constructor( private fun handleQuoteResult(quotes: List) { sendOnrampQuotesErrorAnalytic(quotes) - if (quotes.all { it is OnrampQuote.AmountError }) { - _state.update { amountStateFactory.getAmountSecondaryFieldUpdatedState(quotes) } - } else { - _state.update { amountStateFactory.getAmountSecondaryFieldResetState() } + when { + quotes.isEmpty() -> { + _state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } + } + quotes.all { it is OnrampQuote.AmountError } -> { + _state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) } + } + else -> { + _state.update { amountStateFactory.getAmountSecondaryFieldResetState() } + } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt index cf0d2ca599..1d5565f6a7 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt @@ -73,7 +73,8 @@ private fun InitialLoading(state: OnrampV2MainComponentUM.InitialLoading, modifi Column( modifier = modifier .fillMaxWidth() - .wrapContentHeight(), + .wrapContentHeight() + .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { OnrampAmountContentLoading() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt index 8c6125e33d..6fcafbb281 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt @@ -21,7 +21,7 @@ internal fun PaymentMethodIcon(imageUrl: String, modifier: Modifier = Modifier) .size(TangemTheme.dimens.size40) .clip(TangemTheme.shapes.roundedCorners8) .background(TangemColorPalette.Light1) - .padding(TangemTheme.dimens.spacing6) + .padding(TangemTheme.dimens.spacing4) .testTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_ICON), model = ImageRequest.Builder(context = LocalContext.current) .data(imageUrl)