Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-09 16:48:22 +05:00
commit 781150b16f
12 changed files with 123 additions and 23 deletions

View file

@ -227,6 +227,15 @@ sealed class NotificationUM(val config: NotificationConfig) {
wrappedList(cryptoAmount, fiatAmount),
),
)
data class OnrampErrorNotification(val onRefresh: () -> Unit) : Warning(
title = resourceReference(R.string.common_error),
subtitle = resourceReference(R.string.common_unknown_error),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = onRefresh,
),
)
}
open class Info(

View file

@ -3,6 +3,7 @@ package com.tangem.data.onramp
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.model.error.OnrampPairsError
import com.tangem.domain.onramp.model.error.OnrampRedirectError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
@ -17,6 +18,7 @@ internal class DefaultOnrampErrorResolver(
}
is OnrampRedirectError.WrongRequestId -> OnrampError.RedirectError.WrongRequestId
is OnrampRedirectError.VerificationFailed -> OnrampError.RedirectError.VerificationFailed
is OnrampPairsError.PairsNotFound -> OnrampError.PairsNotFound
else -> {
OnrampError.DomainError(throwable.message)
}

View file

@ -38,6 +38,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.onramp.model.*
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.model.error.OnrampPairsError
import com.tangem.domain.onramp.model.error.OnrampRedirectError
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.Amount
@ -341,6 +342,7 @@ internal class DefaultOnrampRepository(
}
private suspend fun storeOnrampPairs(pairs: List<OnrampPairDTO>, providers: List<ExchangeProvider>) {
if (pairs.isEmpty() || providers.isEmpty()) throw OnrampPairsError.PairsNotFound
val onrampPaymentMethods = getPaymentMethods()
val onrampPairs = pairs.map { pair ->
val onrampProviders = pair.providers.mapNotNull { onrampProviderDTO ->

View file

@ -29,4 +29,6 @@ sealed class OnrampError {
data class DomainError(
val description: String?,
) : OnrampError()
data object PairsNotFound : OnrampError()
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.onramp.model.error
sealed class OnrampPairsError : Throwable() {
data object PairsNotFound : OnrampPairsError()
}

View file

@ -8,4 +8,5 @@ interface OnrampIntents {
fun openCurrenciesList()
fun onBuyClick(quote: OnrampProviderWithQuote.Data)
fun openProviders()
fun onRefresh()
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.onramp.main.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
@ -13,11 +14,13 @@ internal sealed interface OnrampMainComponentUM {
val topBarConfig: OnrampMainTopBarUM
val buyButtonConfig: BuyButtonConfig
val errorNotification: NotificationUM?
data class InitialLoading(
val currency: String,
val onClose: () -> Unit,
val openSettings: () -> Unit,
override val errorNotification: NotificationUM? = null,
) : OnrampMainComponentUM {
override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM(
title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")),
@ -43,6 +46,7 @@ internal sealed interface OnrampMainComponentUM {
data class Content(
override val topBarConfig: OnrampMainTopBarUM,
override val buyButtonConfig: BuyButtonConfig,
override val errorNotification: NotificationUM?,
val amountBlockState: OnrampAmountBlockUM,
val providerBlockState: OnrampProviderBlockUM,
) : OnrampMainComponentUM

View file

@ -5,12 +5,16 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.convertToAmount
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.*
import com.tangem.utils.Provider
import java.math.BigDecimal
@ -38,9 +42,59 @@ internal class OnrampStateFactory(
buyButtonConfig = state.buyButtonConfig,
amountBlockState = getInitialAmountBlockState(currency),
providerBlockState = OnrampProviderBlockUM.Empty,
errorNotification = null,
)
}
fun getOnrampErrorState(onrampError: OnrampError): OnrampMainComponentUM {
return when (onrampError) {
OnrampError.PairsNotFound -> getNoPairsErrorState()
is OnrampError.DataError,
is OnrampError.DomainError,
-> getErrorState()
is OnrampError.AmountError.TooBigError,
is OnrampError.AmountError.TooSmallError,
OnrampError.RedirectError.VerificationFailed,
OnrampError.RedirectError.WrongRequestId,
-> currentStateProvider() // ignore error state
}
}
private fun getNoPairsErrorState(): OnrampMainComponentUM {
val state = currentStateProvider()
val contentState = state as? OnrampMainComponentUM.Content ?: return state
return contentState.copy(
buyButtonConfig = contentState.buyButtonConfig.copy(enabled = false),
amountBlockState = contentState.amountBlockState.copy(
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Error(
error = resourceReference(R.string.onramp_no_available_providers),
),
),
)
}
private fun getErrorState(): OnrampMainComponentUM {
val state = currentStateProvider()
val endButton = state.topBarConfig.endButtonUM.copy(enabled = true)
return when (state) {
is OnrampMainComponentUM.Content -> state.copy(
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
buyButtonConfig = state.buyButtonConfig.copy(enabled = false),
amountBlockState = state.amountBlockState.copy(
amountFieldModel = state.amountBlockState.amountFieldModel.copy(isError = true),
),
providerBlockState = OnrampProviderBlockUM.Empty,
errorNotification = NotificationUM.Warning.OnrampErrorNotification(onrampIntents::onRefresh),
)
is OnrampMainComponentUM.InitialLoading -> state.copy(
errorNotification = NotificationUM.Warning.OnrampErrorNotification(onrampIntents::onRefresh),
)
}
}
private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampAmountBlockUM {
return OnrampAmountBlockUM(
currencyUM = OnrampCurrencyUM(

View file

@ -116,10 +116,7 @@ internal class OnrampMainComponentModel @Inject constructor(
modelScope.launch {
checkOnrampAvailabilityUseCase()
.onRight(::handleOnrampAvailability)
.onLeft { error ->
Timber.e(error.toString())
sendOnrampErrorAnalytic(error)
}
.onLeft(::handleOnrampError)
}
}
@ -136,7 +133,7 @@ internal class OnrampMainComponentModel @Inject constructor(
getOnrampCurrencyUseCase.invoke()
.onEach { maybeCurrency ->
maybeCurrency.fold(
ifLeft = ::sendOnrampErrorAnalytic,
ifLeft = ::handleOnrampError,
ifRight = { currency ->
if (currency == null) return@onEach
_state.update { amountStateFactory.getUpdatedCurrencyState(currency) }
@ -157,10 +154,16 @@ internal class OnrampMainComponentModel @Inject constructor(
}
private suspend fun updatePairsAndQuotes() {
fetchPairsUseCase.invoke(params.cryptoCurrency).onLeft(::sendOnrampErrorAnalytic)
fetchPairsUseCase.invoke(params.cryptoCurrency).onLeft(::handleOnrampError)
startLoadingQuotes()
}
private fun handleOnrampError(onrampError: OnrampError) {
Timber.e(onrampError.toString())
sendOnrampErrorAnalytic(onrampError)
_state.update { stateFactory.getOnrampErrorState(onrampError) }
}
private fun startLoadingQuotes() {
quotesTaskScheduler.cancelTask()
quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask())
@ -176,7 +179,7 @@ internal class OnrampMainComponentModel @Inject constructor(
fetchQuotesUseCase.invoke(
amount = content.amountBlockState.amountFieldModel.fiatAmount,
cryptoCurrency = params.cryptoCurrency,
).onLeft(::sendOnrampErrorAnalytic)
).onLeft(::handleOnrampError)
}
},
onSuccess = {},
@ -188,7 +191,7 @@ internal class OnrampMainComponentModel @Inject constructor(
getOnrampQuotesUseCase.invoke()
.onEach { maybeQuotes ->
maybeQuotes.fold(
ifLeft = ::sendOnrampErrorAnalytic,
ifLeft = ::handleOnrampError,
ifRight = { quotes ->
quotes.filterIsInstance<OnrampQuote.Error>().forEach { errorState ->
sendOnrampErrorAnalytic(errorState.error)
@ -253,6 +256,20 @@ internal class OnrampMainComponentModel @Inject constructor(
)
}
override fun onRefresh() {
_state.update {
stateFactory.getInitialState(
currency = params.cryptoCurrency.name,
onClose = router::pop,
)
}
quotesTaskScheduler.cancelTask()
modelScope.launch {
clearOnrampCacheUseCase.invoke()
checkResidenceCountry()
}
}
override fun onDestroy() {
modelScope.launch { clearOnrampCacheUseCase.invoke() }
quotesTaskScheduler.cancelTask()

View file

@ -30,7 +30,6 @@ import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM
import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM
import com.tangem.features.onramp.main.entity.OnrampCurrencyUM
import kotlinx.coroutines.delay
@Composable
internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier = Modifier) {
@ -85,7 +84,6 @@ private fun OnrampAmountField(amountField: AmountFieldModel) {
)
LaunchedEffect(key1 = Unit) {
delay(timeMillis = 200)
requester.requestFocus()
}
}

View file

@ -16,6 +16,7 @@ import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
@ -43,16 +44,17 @@ internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier:
.fillMaxWidth()
.wrapContentHeight()
when (state) {
is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier)
is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier, state = state)
is OnrampMainComponentUM.Content -> Content(modifier = contentModifier, state = state)
}
},
floatingActionButton = {
BuyButton(
PrimaryButton(
modifier = Modifier
.navigationBarsPadding()
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(id = R.string.common_buy),
onClick = state.buyButtonConfig.onClick,
enabled = state.buyButtonConfig.enabled,
)
@ -62,9 +64,21 @@ internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier:
}
@Composable
private fun InitialLoading(modifier: Modifier = Modifier) {
private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
OnrampAmountContentLoading()
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
}
}
@Composable
private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
.background(TangemTheme.colors.background.action)
.padding(vertical = TangemTheme.dimens.spacing28),
@ -97,15 +111,6 @@ private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = M
) {
OnrampAmountContent(state = state.amountBlockState)
OnrampProviderContent(state = state.providerBlockState, modifier = Modifier.fillMaxWidth())
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
}
}
@Composable
private fun BuyButton(onClick: () -> Unit, enabled: Boolean, modifier: Modifier = Modifier) {
PrimaryButton(
modifier = modifier,
text = stringResource(id = R.string.common_buy),
onClick = onClick,
enabled = enabled,
)
}

View file

@ -28,5 +28,6 @@ internal fun AnalyticsEventHandler.sendOnrampErrorEvent(
is OnrampError.AmountError.TooSmallError,
OnrampError.RedirectError.VerificationFailed,
OnrampError.RedirectError.WrongRequestId,
OnrampError.PairsNotFound,
-> { /* no-op */ }
}