Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-06 17:49:11 +05:00
parent e9ed80c7a4
commit cb62ebfd05
42 changed files with 475 additions and 379 deletions

View file

@ -15,6 +15,7 @@ dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.core.deepLinks.global)
implementation(projects.core.analytics)
/** Common modules */
implementation(projects.data.common)

View file

@ -1,12 +1,43 @@
package com.tangem.data.onramp
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.model.error.OnrampRedirectError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
// TODO: [REDACTED_JIRA]
internal class DefaultOnrampErrorResolver : OnrampErrorResolver {
internal class DefaultOnrampErrorResolver(
private val analyticsEventHandler: AnalyticsEventHandler,
private val onrampErrorConverter: OnrampErrorConverter,
) : OnrampErrorResolver {
override fun resolve(throwable: Throwable): OnrampError {
return OnrampError.UnknownError
val error = when (throwable) {
is ApiResponseError.HttpException -> {
onrampErrorConverter.convert(throwable.errorBody.orEmpty())
}
is OnrampRedirectError.WrongRequestId -> OnrampError.RedirectError.WrongRequestId
is OnrampRedirectError.VerificationFailed -> OnrampError.RedirectError.VerificationFailed
else -> {
OnrampError.DomainError(throwable.message)
}
}
when (error) {
is OnrampError.AmountError.TooBigError -> analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError)
is OnrampError.AmountError.TooSmallError -> analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError)
is OnrampError.DataError -> analyticsEventHandler.send(
OnrampAnalyticsEvent.Errors(
tokenSymbol = "tokenSymbol",
providerName = "providerName",
errorCode = error.code,
),
)
else -> { /* no-op */
}
}
return error
}
}

View file

@ -8,14 +8,14 @@ import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
import com.tangem.data.onramp.converters.PaymentMethodConverter
import com.tangem.data.onramp.converters.StatusConverter
import com.tangem.data.onramp.converters.error.OnrampQuoteErrorInput
import com.tangem.data.onramp.converters.error.OnrampQuotesErrorConverter
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.TangemExpressValues
import com.tangem.datasource.api.express.models.response.ExchangeProvider
import com.tangem.datasource.api.express.models.response.ExchangeProviderType
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO
import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest
@ -37,6 +37,8 @@ import com.tangem.datasource.local.preferences.utils.storeObject
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.OnrampRedirectError
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.CryptoCurrency
@ -63,7 +65,6 @@ internal class DefaultOnrampRepository(
private val paymentMethodsStore: OnrampPaymentMethodsStore,
private val pairsStore: OnrampPairsStore,
private val quotesStore: OnrampQuotesStore,
private val quotesErrorConverter: OnrampQuotesErrorConverter,
private val walletManagersFacade: WalletManagersFacade,
private val dataSignatureVerifier: DataSignatureVerifier,
moshi: Moshi,
@ -74,6 +75,8 @@ internal class DefaultOnrampRepository(
private val statusConverter = StatusConverter()
private val paymentMethodsConverter = PaymentMethodConverter()
private val onrampDataAdapter = moshi.adapter(OnrampDataJson::class.java)
private val onrampErrorAdapter = moshi.adapter(ExpressErrorResponse::class.java)
private val onrampErrorConverter = OnrampErrorConverter(onrampErrorAdapter)
override suspend fun getCurrencies(): List<OnrampCurrency> = withContext(dispatchers.io) {
onrampApi.getCurrencies()
@ -194,6 +197,13 @@ internal class DefaultOnrampRepository(
val fromAmount = amountValue.movePointRight(amount.decimals).toString()
val currency = requireNotNull(getDefaultCurrencySync()) { "Default currency must not be null" }
val country = requireNotNull(getDefaultCountrySync()) { "Default country must not be null" }
val fromOnrampAmount = OnrampAmount(
value = fromAmount
.toBigDecimalOrDefault()
.movePointLeft(currency.precision),
decimals = currency.precision,
symbol = amount.currencySymbol,
)
val quotes: List<OnrampQuote> = pairs.flatMap { pair ->
pair.providers.flatMap { provider ->
provider.paymentMethods.map { paymentMethod ->
@ -212,13 +222,7 @@ internal class DefaultOnrampRepository(
providerId = provider.id,
).bind()
OnrampQuote.Data(
fromAmount = OnrampAmount(
value = response.fromAmount
.toBigDecimalOrDefault()
.movePointLeft(currency.precision),
decimals = currency.precision,
symbol = amount.currencySymbol,
),
fromAmount = fromOnrampAmount,
toAmount = convertToAmount(response.toAmount, cryptoCurrency),
minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency),
maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency),
@ -227,19 +231,12 @@ internal class DefaultOnrampRepository(
)
},
onError = { error ->
if (error is ApiResponseError.HttpException) {
quotesErrorConverter.convert(
OnrampQuoteErrorInput(
errorBody = error.errorBody.orEmpty(),
amount = amount,
paymentMethod = paymentMethod,
provider = provider,
),
)
} else {
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
null
}
convertQuoteError(
error = error,
paymentMethod = paymentMethod,
provider = provider,
fromOnrampAmount = fromOnrampAmount,
)
},
)
}
@ -419,19 +416,44 @@ internal class DefaultOnrampRepository(
default = AppThemeMode.DEFAULT,
)
return when (appTheme) {
AppThemeMode.FORCE_DARK -> "dark"
AppThemeMode.FORCE_DARK -> PROVIDER_THEME_DARK
AppThemeMode.FORCE_LIGHT,
AppThemeMode.FOLLOW_SYSTEM,
-> "light"
-> PROVIDER_THEME_LIGHT
}
}
private fun List<PaymentMethodDTO>.removeApplePay(): List<PaymentMethodDTO> = filterNot { it.id == "apple-pay" }
private fun convertQuoteError(
error: Throwable,
paymentMethod: OnrampPaymentMethod,
provider: OnrampProvider,
fromOnrampAmount: OnrampAmount,
) = if (error is ApiResponseError.HttpException) {
val onrampError = onrampErrorConverter.convert(value = error.errorBody.orEmpty())
if (onrampError is OnrampError.AmountError) {
OnrampQuote.Error(
paymentMethod = paymentMethod,
provider = provider,
fromAmount = fromOnrampAmount,
error = onrampError,
)
} else {
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
null
}
} else {
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
null
}
private companion object {
const val PAYMENT_METHODS_KEY = "onramp_payment_methods"
const val SELECTED_PAYMENT_METHOD_KEY = "onramp_selected_payment_method"
const val PAIRS_KEY = "onramp_pairs"
const val QUOTES_KEY = "onramp_quotes"
const val PROVIDER_THEME_DARK = "dark"
const val PROVIDER_THEME_LIGHT = "light"
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.data.onramp.converters.error
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.api.express.models.response.ExpressError
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.utils.converter.Converter
internal class OnrampErrorConverter(
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
) : Converter<String, OnrampError> {
@Suppress("MagicNumber")
override fun convert(value: String): OnrampError {
try {
val error = jsonAdapter.fromJson(value)?.error ?: return OnrampError.DomainError(value)
return when (error.code) {
2250 -> tryParseExchangeTooSmallAmountError(error = error)
2251 -> tryParseExchangeTooBigAmountError(error = error)
else -> OnrampError.DataError(error.code.toString(), error.description)
}
} catch (e: Exception) {
return OnrampError.DomainError(e.message)
}
}
private fun tryParseExchangeTooSmallAmountError(error: ExpressError): OnrampError {
val minAmount = error.value?.minAmount ?: return OnrampError.DataError(error.code.toString(), error.description)
val decimals = error.value?.decimals ?: return OnrampError.DataError(error.code.toString(), error.description)
val requiredAmount = minAmount.toBigDecimalOrNull()?.movePointLeft(decimals)
?: return OnrampError.DataError(error.code.toString(), error.description)
return OnrampError.AmountError.TooSmallError(requiredAmount = requiredAmount)
}
private fun tryParseExchangeTooBigAmountError(error: ExpressError): OnrampError {
val maxAmount = error.value?.maxAmount ?: return OnrampError.DataError(error.code.toString(), error.description)
val decimals = error.value?.decimals ?: return OnrampError.DataError(error.code.toString(), error.description)
val requiredAmount = maxAmount.toBigDecimalOrNull()?.movePointLeft(decimals)
?: return OnrampError.DataError(error.code.toString(), error.description)
return OnrampError.AmountError.TooBigError(requiredAmount = requiredAmount)
}
}

View file

@ -1,12 +0,0 @@
package com.tangem.data.onramp.converters.error
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampProvider
import com.tangem.domain.tokens.model.Amount
data class OnrampQuoteErrorInput(
val errorBody: String,
val amount: Amount,
val paymentMethod: OnrampPaymentMethod,
val provider: OnrampProvider,
)

View file

@ -1,84 +0,0 @@
package com.tangem.data.onramp.converters.error
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.api.express.models.response.ExpressError
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.domain.onramp.model.OnrampAmount
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
internal class OnrampQuotesErrorConverter(
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
) : Converter<OnrampQuoteErrorInput, OnrampQuote.Error?> {
@Suppress("MagicNumber")
override fun convert(value: OnrampQuoteErrorInput): OnrampQuote.Error? {
try {
val error = jsonAdapter.fromJson(value.errorBody)?.error ?: return null
return when (error.code) {
2250 -> tryParseExchangeTooSmallAmountError(error = error, input = value)
2251 -> tryParseExchangeTooBigAmountError(error = error, input = value)
else -> null
}
} catch (e: Exception) {
return null
}
}
private fun tryParseExchangeTooSmallAmountError(
error: ExpressError,
input: OnrampQuoteErrorInput,
): OnrampQuote.Error.AmountTooSmallError? {
val minAmount = error.value?.minAmount ?: return null
val decimals = error.value?.decimals ?: return null
return OnrampQuote.Error.AmountTooSmallError(
paymentMethod = input.paymentMethod,
provider = input.provider,
fromAmount = OnrampAmount(
symbol = input.amount.currencySymbol,
value = input.amount.value.orZero(),
decimals = input.amount.decimals,
),
requiredAmount = createFromAmountWithOffset(
amountWithOffset = minAmount,
decimals = decimals,
symbol = input.amount.currencySymbol,
),
)
}
private fun tryParseExchangeTooBigAmountError(
error: ExpressError,
input: OnrampQuoteErrorInput,
): OnrampQuote.Error.AmountTooBigError? {
val maxAmount = error.value?.maxAmount ?: return null
val decimals = error.value?.decimals ?: return null
return OnrampQuote.Error.AmountTooBigError(
paymentMethod = input.paymentMethod,
provider = input.provider,
fromAmount = OnrampAmount(
symbol = input.amount.currencySymbol,
value = input.amount.value.orZero(),
decimals = input.amount.decimals,
),
requiredAmount = createFromAmountWithOffset(
amountWithOffset = maxAmount,
decimals = decimals,
symbol = input.amount.currencySymbol,
),
)
}
private fun createFromAmountWithOffset(amountWithOffset: String, decimals: Int, symbol: String): OnrampAmount {
return OnrampAmount(
value = requireNotNull(
amountWithOffset.toBigDecimalOrNull()?.movePointLeft(decimals),
) { "wrong amount format, use only digits" },
decimals = decimals,
symbol = symbol,
)
}
}

View file

@ -1,10 +1,11 @@
package com.tangem.data.onramp.di
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
import com.tangem.data.onramp.converters.error.OnrampQuotesErrorConverter
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.onramp.OnrampApi
@ -39,7 +40,6 @@ internal object OnrampDataModule {
paymentMethodsStore: OnrampPaymentMethodsStore,
pairsStore: OnrampPairsStore,
quotesStore: OnrampQuotesStore,
quotesErrorConverter: OnrampQuotesErrorConverter,
walletManagersFacade: WalletManagersFacade,
dataSignatureVerifier: DataSignatureVerifier,
@NetworkMoshi moshi: Moshi,
@ -52,7 +52,6 @@ internal object OnrampDataModule {
paymentMethodsStore = paymentMethodsStore,
pairsStore = pairsStore,
quotesStore = quotesStore,
quotesErrorConverter = quotesErrorConverter,
walletManagersFacade = walletManagersFacade,
dataSignatureVerifier = dataSignatureVerifier,
moshi = moshi,
@ -73,14 +72,14 @@ internal object OnrampDataModule {
@Provides
@Singleton
fun provideOnrampErrorResolver(): OnrampErrorResolver {
return DefaultOnrampErrorResolver()
}
@Provides
@Singleton
internal fun provideErrorsConverter(@NetworkMoshi moshi: Moshi): OnrampQuotesErrorConverter {
fun provideOnrampErrorResolver(
@NetworkMoshi moshi: Moshi,
analyticsEventHandler: AnalyticsEventHandler,
): OnrampErrorResolver {
val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java)
return OnrampQuotesErrorConverter(jsonAdapter)
return DefaultOnrampErrorResolver(
analyticsEventHandler,
OnrampErrorConverter(jsonAdapter),
)
}
}