Updated on 2026-08-14
This commit is contained in:
commit
781150b16f
12 changed files with 123 additions and 23 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -29,4 +29,6 @@ sealed class OnrampError {
|
|||
data class DomainError(
|
||||
val description: String?,
|
||||
) : OnrampError()
|
||||
|
||||
data object PairsNotFound : OnrampError()
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.onramp.model.error
|
||||
|
||||
sealed class OnrampPairsError : Throwable() {
|
||||
data object PairsNotFound : OnrampPairsError()
|
||||
}
|
||||
|
|
@ -8,4 +8,5 @@ interface OnrampIntents {
|
|||
fun openCurrenciesList()
|
||||
fun onBuyClick(quote: OnrampProviderWithQuote.Data)
|
||||
fun openProviders()
|
||||
fun onRefresh()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -28,5 +28,6 @@ internal fun AnalyticsEventHandler.sendOnrampErrorEvent(
|
|||
is OnrampError.AmountError.TooSmallError,
|
||||
OnrampError.RedirectError.VerificationFailed,
|
||||
OnrampError.RedirectError.WrongRequestId,
|
||||
OnrampError.PairsNotFound,
|
||||
-> { /* no-op */ }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue