Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-19 22:17:32 +05:00
parent 550f404055
commit ff42bcfcb5
6 changed files with 123 additions and 44 deletions

View file

@ -0,0 +1,8 @@
package com.tangem.features.onramp.main.entity
import com.tangem.domain.onramp.model.OnrampAmount
data class OnrampLastUpdate(
val lastAmount: OnrampAmount,
val lastCountryString: String,
)

View file

@ -49,8 +49,11 @@ internal class OnrampStateFactory(
fun getOnrampErrorState(onrampError: OnrampError): OnrampMainComponentUM {
return when (onrampError) {
OnrampError.PairsNotFound -> getNoPairsErrorState()
is OnrampError.DataError -> getErrorState(onrampError.code)
is OnrampError.DomainError -> getErrorState()
is OnrampError.DataError -> getErrorState(
errorCode = onrampError.code,
onRefresh = onrampIntents::onRefresh,
)
is OnrampError.DomainError -> getErrorState(onRefresh = onrampIntents::onRefresh)
is OnrampError.AmountError.TooBigError,
is OnrampError.AmountError.TooSmallError,
OnrampError.RedirectError.VerificationFailed,
@ -74,7 +77,7 @@ internal class OnrampStateFactory(
)
}
fun getErrorState(errorCode: String? = null): OnrampMainComponentUM {
fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampMainComponentUM {
val state = currentStateProvider()
val endButton = state.topBarConfig.endButtonUM.copy(enabled = true)
@ -89,13 +92,13 @@ internal class OnrampStateFactory(
providerBlockState = OnrampProviderBlockUM.Empty,
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
errorCode = errorCode,
onRefresh = onrampIntents::onRefresh,
onRefresh = onRefresh,
),
)
is OnrampMainComponentUM.InitialLoading -> state.copy(
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
errorCode = errorCode,
onRefresh = onrampIntents::onRefresh,
onRefresh = onRefresh,
),
)
}

View file

@ -14,15 +14,18 @@ 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.tokens.model.AmountType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.*
import com.tangem.features.onramp.providers.entity.SelectProviderResult
import com.tangem.utils.Provider
import com.tangem.utils.extensions.isSingleItem
internal class OnrampAmountStateFactory(
private val currentStateProvider: Provider<OnrampMainComponentUM>,
private val analyticsEventHandler: AnalyticsEventHandler,
private val onrampIntents: OnrampIntents,
private val cryptoCurrency: CryptoCurrency,
) {
private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter(
@ -62,11 +65,12 @@ internal class OnrampAmountStateFactory(
return currentState.copy(
amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading),
providerBlockState = OnrampProviderBlockUM.Loading,
buyButtonConfig = currentState.buyButtonConfig.copy(enabled = false),
)
}
fun getAmountSecondaryUpdatedState(quote: OnrampQuote, isBestRate: Boolean): OnrampMainComponentUM {
fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM {
val currentState = currentStateProvider()
if (currentState !is OnrampMainComponentUM.Content) return currentState
@ -75,9 +79,9 @@ internal class OnrampAmountStateFactory(
return currentState.copy(
amountBlockState = amountState.copy(
amountFieldModel = amountState.amountFieldModel.copy(isError = false),
secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel,
),
providerBlockState = quote.toProviderBlockState(isBestRate),
buyButtonConfig = currentState.buyButtonConfig.copy(
enabled = quote is OnrampQuote.Data,
onClick = {
@ -96,6 +100,32 @@ internal class OnrampAmountStateFactory(
)
}
fun getUpdatedProviderState(selectedQuote: OnrampQuote, quotes: List<OnrampQuote>): OnrampMainComponentUM {
val currentState = currentStateProvider()
if (currentState !is OnrampMainComponentUM.Content) return currentState
analyticsEventHandler.send(
OnrampAnalyticsEvent.ProviderCalculated(
providerName = selectedQuote.provider.info.name,
tokenSymbol = cryptoCurrency.symbol,
paymentMethod = selectedQuote.paymentMethod.name,
),
)
val bestProvider = selectedQuote as? OnrampQuote.Data
val isMultipleQuotes = !quotes.isSingleItem()
val isOtherQuotesHasData = quotes
.filter { it.paymentMethod == selectedQuote.paymentMethod }
.filterNot { it == bestProvider }
.any { it is OnrampQuote.Data }
val isBestProvider = selectedQuote == bestProvider && isMultipleQuotes && isOtherQuotesHasData
return currentState.copy(
providerBlockState = selectedQuote.toProviderBlockState(isBestProvider),
)
}
fun getAmountSecondaryUpdatedState(
providerResult: SelectProviderResult,
isBestRate: Boolean,

View file

@ -25,10 +25,7 @@ import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.OnrampMainComponent
import com.tangem.features.onramp.main.entity.OnrampIntents
import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
import com.tangem.features.onramp.main.entity.*
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory
import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory
import com.tangem.features.onramp.providers.entity.SelectProviderResult
@ -38,15 +35,13 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.PeriodicTask
import com.tangem.utils.coroutines.SingleTaskScheduler
import com.tangem.utils.extensions.isSingleItem
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class OnrampMainComponentModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
@ -75,6 +70,7 @@ internal class OnrampMainComponentModel @Inject constructor(
currentStateProvider = Provider { _state.value },
analyticsEventHandler = analyticsEventHandler,
onrampIntents = this,
cryptoCurrency = params.cryptoCurrency,
)
private val selectedUserWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
private val _state: MutableStateFlow<OnrampMainComponentUM> = MutableStateFlow(
@ -87,7 +83,7 @@ internal class OnrampMainComponentModel @Inject constructor(
val state: StateFlow<OnrampMainComponentUM> get() = _state.asStateFlow()
val bottomSheetNavigation: SlotNavigation<OnrampMainBottomSheetConfig> = SlotNavigation()
private val lastAmount = mutableStateOf(BigDecimal.ZERO)
private val lastUpdateState = mutableStateOf<OnrampLastUpdate?>(null)
init {
sendScreenOpenAnalytics()
@ -161,7 +157,11 @@ internal class OnrampMainComponentModel @Inject constructor(
}
private suspend fun updatePairsAndQuotes() {
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
val state = state.value as? OnrampMainComponentUM.Content
if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) {
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
}
fetchPairsUseCase.invoke(params.cryptoCurrency).fold(
ifLeft = ::handleOnrampError,
ifRight = { _state.update { amountStateFactory.getAmountSecondaryResetState() } },
@ -200,7 +200,6 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun subscribeToQuotesUpdate() {
getOnrampQuotesUseCase.invoke()
.distinctUntilChanged()
.conflate()
.onEach { maybeQuotes ->
maybeQuotes.fold(
@ -282,42 +281,67 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun handleQuoteResult(quotes: List<OnrampQuote>) {
sendOnrampQuotesErrorAnalytic(quotes)
val quote = quotes.firstOrNull { it !is OnrampQuote.Error }
val quote = selectOrUpdateQuote(quotes)
if (quote == null) {
_state.update { stateFactory.getErrorState() }
lastAmount.value = BigDecimal.ZERO
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
lastUpdateState.value = null
return
}
val bestProvider = quote as? OnrampQuote.Data
val isMultipleQuotes = !quotes.isSingleItem()
val isOtherQuotesHasData = quotes
.filter { it.paymentMethod == quote.paymentMethod }
.filterNot { it == bestProvider }
.any { it is OnrampQuote.Data }
val hasBestProvider = isMultipleQuotes && isOtherQuotesHasData
if (checkLastInputState(quote)) {
lastUpdateState.value = OnrampLastUpdate(
quote.fromAmount,
quote.countryCode,
)
val isBestProvider = quote == bestProvider && hasBestProvider
if (lastAmount.value != quote.fromAmount.value) {
lastAmount.value = quote.fromAmount.value
if (quote is OnrampQuote.Data) {
analyticsEventHandler.send(
OnrampAnalyticsEvent.ProviderCalculated(
providerName = quote.provider.info.name,
tokenSymbol = params.cryptoCurrency.symbol,
paymentMethod = quote.paymentMethod.name,
),
)
}
_state.update {
amountStateFactory.getAmountSecondaryUpdatedState(
quote = quote,
isBestRate = isBestProvider,
)
amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes)
}
}
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) }
}
/**
* !!! Important quote selection logic !!!
* Selects or updated quote based on input data (amount, country, currency).
* If input data has changed select new best quote, otherwise last selected quote.
* If last selected quote on same input data is in an error state, select next best quote
* If new best quote or next best quote does not exist (i.e. Error state) select nothing.
*/
private fun selectOrUpdateQuote(quotes: List<OnrampQuote>): OnrampQuote? {
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
// Check if amount, country or currency has changed
return if (checkLastInputState(quoteToCheck)) {
quoteToCheck
} else {
val state = state.value as? OnrampMainComponentUM.Content
val providerState = state?.providerBlockState as? OnrampProviderBlockUM.Content
// Get current selected quote to update
val lastSelectedQuote = quotes.firstOrNull {
it.provider.id == providerState?.providerId &&
it.paymentMethod.id == providerState.paymentMethod.id
}
// Check if selected updated quote is not error
if (lastSelectedQuote is OnrampQuote.Error) {
quoteToCheck
} else {
lastSelectedQuote
}
}
}
private fun onRetryQuotes() {
_state.update {
(it as? OnrampMainComponentUM.Content)?.copy(
errorNotification = null,
providerBlockState = OnrampProviderBlockUM.Loading,
) ?: it
}
startLoadingQuotes()
}
private fun showDemoWarning() {
@ -380,6 +404,11 @@ internal class OnrampMainComponentModel @Inject constructor(
}
}
private fun checkLastInputState(quote: OnrampQuote?): Boolean {
return lastUpdateState.value?.lastAmount != quote?.fromAmount ||
lastUpdateState.value?.lastCountryString != quote?.countryCode
}
private companion object {
const val UPDATE_DELAY = 10_000L
}