Updated on 2026-08-14
This commit is contained in:
parent
e9ed80c7a4
commit
cb62ebfd05
42 changed files with 475 additions and 379 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue