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

@ -18,38 +18,56 @@ internal object OnrampDomainModule {
@Provides
@Singleton
fun provideGetOnrampCurrenciesUseCase(onrampRepository: OnrampRepository): GetOnrampCurrenciesUseCase {
return GetOnrampCurrenciesUseCase(onrampRepository)
fun provideGetOnrampCurrenciesUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampCurrenciesUseCase {
return GetOnrampCurrenciesUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampSaveDefaultCurrencyUseCase(onrampRepository: OnrampRepository): OnrampSaveDefaultCurrencyUseCase {
return OnrampSaveDefaultCurrencyUseCase(onrampRepository)
fun provideOnrampSaveDefaultCurrencyUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): OnrampSaveDefaultCurrencyUseCase {
return OnrampSaveDefaultCurrencyUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetOnrampCountriesUseCase(onrampRepository: OnrampRepository): GetOnrampCountriesUseCase {
return GetOnrampCountriesUseCase(onrampRepository)
fun provideGetOnrampCountriesUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampCountriesUseCase {
return GetOnrampCountriesUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetOnrampCountryUseCase(onrampRepository: OnrampRepository): GetOnrampCountryUseCase {
return GetOnrampCountryUseCase(onrampRepository)
fun provideGetOnrampCountryUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampCountryUseCase {
return GetOnrampCountryUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampSaveDefaultCountryUseCase(onrampRepository: OnrampRepository): OnrampSaveDefaultCountryUseCase {
return OnrampSaveDefaultCountryUseCase(onrampRepository)
fun provideOnrampSaveDefaultCountryUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): OnrampSaveDefaultCountryUseCase {
return OnrampSaveDefaultCountryUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideCheckOnrampAvailabilityUseCase(onrampRepository: OnrampRepository): CheckOnrampAvailabilityUseCase {
return CheckOnrampAvailabilityUseCase(onrampRepository)
fun provideCheckOnrampAvailabilityUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): CheckOnrampAvailabilityUseCase {
return CheckOnrampAvailabilityUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@ -66,54 +84,65 @@ internal object OnrampDomainModule {
@Provides
@Singleton
fun provideGetOnrampCurrencyUseCase(onrampRepository: OnrampRepository): GetOnrampCurrencyUseCase {
return GetOnrampCurrencyUseCase(onrampRepository)
fun provideGetOnrampCurrencyUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampCurrencyUseCase {
return GetOnrampCurrencyUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetOnrampTransactionsUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampTransactionsUseCase {
return GetOnrampTransactionsUseCase(onrampTransactionRepository)
return GetOnrampTransactionsUseCase(onrampTransactionRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetOnrampTransactionUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampTransactionUseCase {
return GetOnrampTransactionUseCase(onrampTransactionRepository)
return GetOnrampTransactionUseCase(onrampTransactionRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampRemoveTransactionUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
onrampErrorResolver: OnrampErrorResolver,
): OnrampRemoveTransactionUseCase {
return OnrampRemoveTransactionUseCase(onrampTransactionRepository)
return OnrampRemoveTransactionUseCase(onrampTransactionRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampSaveTransactionUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
onrampErrorResolver: OnrampErrorResolver,
): OnrampSaveTransactionUseCase {
return OnrampSaveTransactionUseCase(onrampTransactionRepository)
return OnrampSaveTransactionUseCase(onrampTransactionRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampUpdateTransactionStatusUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
onrampErrorResolver: OnrampErrorResolver,
): OnrampUpdateTransactionStatusUseCase {
return OnrampUpdateTransactionStatusUseCase(onrampTransactionRepository)
return OnrampUpdateTransactionStatusUseCase(onrampTransactionRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetOnrampPaymentMethodsUseCase(onrampRepository: OnrampRepository): GetOnrampPaymentMethodsUseCase {
return GetOnrampPaymentMethodsUseCase(onrampRepository)
fun provideGetOnrampPaymentMethodsUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampPaymentMethodsUseCase {
return GetOnrampPaymentMethodsUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@ -124,8 +153,11 @@ internal object OnrampDomainModule {
@Provides
@Singleton
fun provideOnrampFetchQuotesUseCase(onrampRepository: OnrampRepository): OnrampFetchQuotesUseCase {
return OnrampFetchQuotesUseCase(onrampRepository)
fun provideOnrampFetchQuotesUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): OnrampFetchQuotesUseCase {
return OnrampFetchQuotesUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@ -133,10 +165,12 @@ internal object OnrampDomainModule {
fun provideGetOnrampQuotesUseCase(
settingsRepository: SettingsRepository,
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampQuotesUseCase {
return GetOnrampQuotesUseCase(
settingsRepository = settingsRepository,
repository = onrampRepository,
errorResolver = onrampErrorResolver,
)
}
@ -144,28 +178,36 @@ internal object OnrampDomainModule {
@Singleton
fun provideGetOnrampSelectedPaymentMethodUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampSelectedPaymentMethodUseCase {
return GetOnrampSelectedPaymentMethodUseCase(onrampRepository)
return GetOnrampSelectedPaymentMethodUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetOnrampProviderWithQuoteUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampProviderWithQuoteUseCase {
return GetOnrampProviderWithQuoteUseCase(onrampRepository)
return GetOnrampProviderWithQuoteUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampSaveSelectedPaymentMethod(onrampRepository: OnrampRepository): OnrampSaveSelectedPaymentMethod {
return OnrampSaveSelectedPaymentMethod(onrampRepository)
fun provideOnrampSaveSelectedPaymentMethod(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): OnrampSaveSelectedPaymentMethod {
return OnrampSaveSelectedPaymentMethod(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampFetchPairsUseCase(onrampRepository: OnrampRepository): OnrampFetchPairsUseCase {
return OnrampFetchPairsUseCase(onrampRepository)
fun provideOnrampFetchPairsUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): OnrampFetchPairsUseCase {
return OnrampFetchPairsUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@ -173,7 +215,8 @@ internal object OnrampDomainModule {
fun provideGetOnrampRedirectUrlUseCase(
onrampRepository: OnrampRepository,
transactionRepository: OnrampTransactionRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampRedirectUrlUseCase {
return GetOnrampRedirectUrlUseCase(onrampRepository, transactionRepository)
return GetOnrampRedirectUrlUseCase(onrampRepository, transactionRepository, onrampErrorResolver)
}
}

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),
)
}
}

View file

@ -1,5 +0,0 @@
package com.tangem.domain.onramp.model
sealed class OnrampError {
data object UnknownError : OnrampError()
}

View file

@ -21,20 +21,9 @@ sealed interface OnrampProviderWithQuote {
val availablePaymentMethods: List<OnrampPaymentMethod>,
) : Unavailable
data class AvailableFrom(
data class Error(
override val provider: OnrampProvider,
val paymentMethod: OnrampPaymentMethod,
val quoteError: OnrampQuote.Error,
val fromAmount: OnrampAmount,
val requiredAmount: OnrampAmount,
) : Unavailable
data class AvailableUpTo(
override val provider: OnrampProvider,
val paymentMethod: OnrampPaymentMethod,
val quoteError: OnrampQuote.Error,
val fromAmount: OnrampAmount,
val requiredAmount: OnrampAmount,
) : Unavailable
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.domain.onramp.model
import com.tangem.domain.onramp.model.error.OnrampError
sealed class OnrampQuote {
abstract val paymentMethod: OnrampPaymentMethod
@ -14,20 +16,10 @@ sealed class OnrampQuote {
val maxFromAmount: OnrampAmount,
) : OnrampQuote()
sealed class Error : OnrampQuote() {
data class AmountTooSmallError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val fromAmount: OnrampAmount,
val requiredAmount: OnrampAmount,
) : Error()
data class AmountTooBigError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val fromAmount: OnrampAmount,
val requiredAmount: OnrampAmount,
) : Error()
}
data class Error(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val fromAmount: OnrampAmount,
val error: OnrampError.AmountError,
) : OnrampQuote()
}

View file

@ -0,0 +1,29 @@
package com.tangem.domain.onramp.model.error
import java.math.BigDecimal
sealed class OnrampError {
sealed class AmountError : OnrampError() {
abstract val requiredAmount: BigDecimal
data class TooSmallError(
override val requiredAmount: BigDecimal,
) : AmountError()
data class TooBigError(
override val requiredAmount: BigDecimal,
) : AmountError()
}
sealed class RedirectError : OnrampError() {
data object VerificationFailed : RedirectError()
data object WrongRequestId : RedirectError()
}
data class DataError(
val code: String,
val description: String?,
) : OnrampError()
data class DomainError(val error: String?) : OnrampError()
}

View file

@ -1,9 +1,7 @@
package com.tangem.domain.onramp.model
package com.tangem.domain.onramp.model.error
sealed class OnrampRedirectError : Throwable() {
data class DataError(override val cause: Throwable?) : OnrampRedirectError()
data object VerificationFailed : OnrampRedirectError()
data object WrongRequestId : OnrampRedirectError()

View file

@ -3,11 +3,16 @@ package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampAvailability
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class CheckOnrampAvailabilityUseCase(private val repository: OnrampRepository) {
class CheckOnrampAvailabilityUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(): Either<Throwable, OnrampAvailability> {
suspend operator fun invoke(): Either<OnrampError, OnrampAvailability> {
return Either.catch {
repository.fetchPaymentMethodsIfAbsent()
val savedCountry = repository.getDefaultCountrySync()
@ -17,7 +22,7 @@ class CheckOnrampAvailabilityUseCase(private val repository: OnrampRepository) {
val detectedCountry = repository.getCountryByIp()
OnrampAvailability.ConfirmResidency(detectedCountry)
}
}
}.mapLeft(errorResolver::resolve)
}
private suspend fun proceedWithSavedCountry(savedCountry: OnrampCountry): OnrampAvailability {

View file

@ -2,11 +2,16 @@ package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class GetOnrampCountriesUseCase(private val onrampRepository: OnrampRepository) {
class GetOnrampCountriesUseCase(
private val onrampRepository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(): Either<Throwable, List<OnrampCountry>> {
return Either.catch { onrampRepository.getCountries() }
suspend operator fun invoke(): Either<OnrampError, List<OnrampCountry>> {
return Either.catch { onrampRepository.getCountries() }.mapLeft(errorResolver::resolve)
}
}

View file

@ -4,20 +4,28 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
class GetOnrampCountryUseCase(private val repository: OnrampRepository) {
class GetOnrampCountryUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
operator fun invoke(): Flow<Either<Throwable, OnrampCountry?>> {
operator fun invoke(): Flow<Either<OnrampError, OnrampCountry?>> {
return repository.getDefaultCountry()
.map<OnrampCountry?, Either<Throwable, OnrampCountry?>> { it.right() }
.catch { emit(it.left()) }
.map<OnrampCountry?, Either<OnrampError, OnrampCountry?>> { it.right() }
.catch {
emit(errorResolver.resolve(it).left())
}
}
suspend fun invokeSync(): Either<Throwable, OnrampCountry> {
suspend fun invokeSync(): Either<OnrampError, OnrampCountry> {
return Either.catch { repository.getDefaultCountrySync() ?: repository.getCountryByIp() }
.mapLeft(errorResolver::resolve)
}
}

View file

@ -2,19 +2,22 @@ package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampCurrencies
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class GetOnrampCurrenciesUseCase(
private val onrampRepository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(): Either<Throwable, OnrampCurrencies> {
suspend operator fun invoke(): Either<OnrampError, OnrampCurrencies> {
return Either.catch {
val currenciesList = onrampRepository.getCurrencies()
val (populars, others) = currenciesList.toSet()
.partition { popularFiatCodes.contains(it.code.uppercase()) }
OnrampCurrencies(populars = populars, others = others)
}
}.mapLeft(errorResolver::resolve)
}
private companion object {

View file

@ -4,16 +4,23 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
class GetOnrampCurrencyUseCase(private val repository: OnrampRepository) {
class GetOnrampCurrencyUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
operator fun invoke(): Flow<Either<Throwable, OnrampCurrency?>> {
operator fun invoke(): Flow<Either<OnrampError, OnrampCurrency?>> {
return repository.getDefaultCurrency()
.map<OnrampCurrency?, Either<Throwable, OnrampCurrency?>> { it.right() }
.catch { emit(it.left()) }
.map<OnrampCurrency?, Either<OnrampError, OnrampCurrency?>> { it.right() }
.catch {
emit(errorResolver.resolve(it).left())
}
}
}

View file

@ -2,11 +2,17 @@ package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class GetOnrampPaymentMethodsUseCase(private val repository: OnrampRepository) {
class GetOnrampPaymentMethodsUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(): Either<Throwable, Set<OnrampPaymentMethod>> {
suspend operator fun invoke(): Either<OnrampError, Set<OnrampPaymentMethod>> {
return Either.catch { repository.getAvailablePaymentMethods() }
.mapLeft(errorResolver::resolve)
}
}

View file

@ -6,11 +6,18 @@ import com.tangem.domain.onramp.model.OnrampProvider
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.OnrampProviderWithQuote.Unavailable
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class GetOnrampProviderWithQuoteUseCase(private val repository: OnrampRepository) {
class GetOnrampProviderWithQuoteUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(paymentMethod: OnrampPaymentMethod): Either<Throwable, List<OnrampProviderWithQuote>> {
suspend operator fun invoke(
paymentMethod: OnrampPaymentMethod,
): Either<OnrampError, List<OnrampProviderWithQuote>> {
return Either.catch {
val quotes: List<OnrampQuote> = requireNotNull(repository.getQuotesSync()) { "Quotes must not be null" }
quotes
@ -18,7 +25,7 @@ class GetOnrampProviderWithQuoteUseCase(private val repository: OnrampRepository
.mapNotNull { (provider, quotes) ->
quotes.quoteWithProvider(provider = provider, selectedPaymentMethod = paymentMethod)
}
}
}.mapLeft(errorResolver::resolve)
}
private fun List<OnrampQuote>.quoteWithProvider(
@ -27,46 +34,29 @@ class GetOnrampProviderWithQuoteUseCase(private val repository: OnrampRepository
): OnrampProviderWithQuote? {
val quoteData = this.filterIsInstance<OnrampQuote.Data>()
val matchedPaymentMethodQuote = quoteData.firstOrNull { it.paymentMethod == selectedPaymentMethod }
if (matchedPaymentMethodQuote != null) {
return OnrampProviderWithQuote.Data(
provider = matchedPaymentMethodQuote.provider,
paymentMethod = matchedPaymentMethodQuote.paymentMethod,
toAmount = matchedPaymentMethodQuote.toAmount,
fromAmount = matchedPaymentMethodQuote.fromAmount,
)
}
val paymentMethodNotSupported = quoteData.filterNot { it.paymentMethod == selectedPaymentMethod }
if (paymentMethodNotSupported.isNotEmpty()) {
return Unavailable.NotSupportedPaymentMethod(
provider = provider,
availablePaymentMethods = paymentMethodNotSupported.map(OnrampQuote.Data::paymentMethod),
)
}
val amountError = this
.filterIsInstance<OnrampQuote.Error>()
.firstOrNull { it.paymentMethod == selectedPaymentMethod }
val amountError =
filterIsInstance<OnrampQuote.Error>().firstOrNull { it.paymentMethod == selectedPaymentMethod }
return when {
matchedPaymentMethodQuote != null -> {
OnrampProviderWithQuote.Data(
provider = matchedPaymentMethodQuote.provider,
paymentMethod = matchedPaymentMethodQuote.paymentMethod,
toAmount = matchedPaymentMethodQuote.toAmount,
fromAmount = matchedPaymentMethodQuote.fromAmount,
)
}
paymentMethodNotSupported.isNotEmpty() -> {
Unavailable.NotSupportedPaymentMethod(
provider = provider,
availablePaymentMethods = paymentMethodNotSupported.map(OnrampQuote.Data::paymentMethod),
)
}
amountError != null -> {
when (amountError) {
is OnrampQuote.Error.AmountTooBigError -> Unavailable.AvailableUpTo(
provider = amountError.provider,
paymentMethod = amountError.paymentMethod,
quoteError = amountError,
fromAmount = amountError.fromAmount,
requiredAmount = amountError.requiredAmount,
)
is OnrampQuote.Error.AmountTooSmallError -> Unavailable.AvailableFrom(
provider = amountError.provider,
paymentMethod = amountError.paymentMethod,
quoteError = amountError,
fromAmount = amountError.fromAmount,
requiredAmount = amountError.requiredAmount,
)
}
Unavailable.Error(
amountError.provider,
amountError,
)
}
else -> null
}

View file

@ -5,6 +5,8 @@ import arrow.core.left
import arrow.core.right
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import kotlinx.coroutines.flow.Flow
@ -14,11 +16,12 @@ import kotlinx.coroutines.flow.map
class GetOnrampQuotesUseCase(
private val settingsRepository: SettingsRepository,
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
operator fun invoke(): Flow<Either<Throwable, List<OnrampQuote>>> {
operator fun invoke(): Flow<Either<OnrampError, List<OnrampQuote>>> {
return repository.getQuotes()
.map<List<OnrampQuote>, Either<Throwable, List<OnrampQuote>>> { quotes ->
.map<List<OnrampQuote>, Either<OnrampError, List<OnrampQuote>>> { quotes ->
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
quotes.groupBy { it.paymentMethod.type }
@ -29,7 +32,9 @@ class GetOnrampQuotesUseCase(
.flatten()
.right()
}
.catch { emit(it.left()) }
.catch {
emit(errorResolver.resolve(it).left())
}
}
/**
@ -44,8 +49,13 @@ class GetOnrampQuotesUseCase(
is OnrampQuote.Data -> it.toAmount.value
// negative difference to sort both when data and unavailable is present
is OnrampQuote.Error.AmountTooSmallError -> it.fromAmount.value - it.requiredAmount.value
is OnrampQuote.Error.AmountTooBigError -> it.requiredAmount.value - it.fromAmount.value
is OnrampQuote.Error -> {
when (val error = it.error) {
is OnrampError.AmountError.TooSmallError -> it.fromAmount.value - error.requiredAmount
is OnrampError.AmountError.TooBigError -> error.requiredAmount - it.fromAmount.value
else -> null
}
}
}
}
}

View file

@ -2,7 +2,8 @@ package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.OnrampRedirectError
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.tokens.model.CryptoCurrency
@ -11,13 +12,14 @@ import com.tangem.domain.wallets.models.UserWalletId
class GetOnrampRedirectUrlUseCase(
private val repository: OnrampRepository,
private val transactionRepository: OnrampTransactionRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
quote: OnrampProviderWithQuote.Data,
cryptoCurrency: CryptoCurrency,
): Either<OnrampRedirectError, String> {
): Either<OnrampError, String> {
return Either.catch {
val transaction = repository.getOnrampData(
userWalletId = userWalletId,
@ -26,8 +28,6 @@ class GetOnrampRedirectUrlUseCase(
)
transactionRepository.storeTransaction(transaction)
transaction.redirectUrl
}.mapLeft { throwable ->
if (throwable is OnrampRedirectError) throwable else OnrampRedirectError.DataError(throwable)
}
}.mapLeft(errorResolver::resolve)
}
}

View file

@ -5,15 +5,22 @@ import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
class GetOnrampSelectedPaymentMethodUseCase(private val repository: OnrampRepository) {
class GetOnrampSelectedPaymentMethodUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
operator fun invoke(): EitherFlow<Throwable, OnrampPaymentMethod> {
operator fun invoke(): EitherFlow<OnrampError, OnrampPaymentMethod> {
return repository.getSelectedPaymentMethod()
.map<OnrampPaymentMethod, Either<Throwable, OnrampPaymentMethod>> { it.right() }
.catch { emit(it.left()) }
.map<OnrampPaymentMethod, Either<OnrampError, OnrampPaymentMethod>> { it.right() }
.catch {
emit(errorResolver.resolve(it).left())
}
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
@ -14,8 +14,6 @@ class GetOnrampStatusUseCase(
suspend operator fun invoke(externalTxId: String): Either<OnrampError, OnrampStatus> {
return Either.catch {
onrampRepository.getStatus(externalTxId)
}.mapLeft {
errorResolver.resolve(it)
}
}.mapLeft(errorResolver::resolve)
}
}

View file

@ -1,12 +1,14 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
class GetOnrampTransactionUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(externalTxId: String): Either<OnrampError, OnrampTransaction> {
@ -14,8 +16,6 @@ class GetOnrampTransactionUseCase(
requireNotNull(
onrampTransactionRepository.getTransactionById(externalTxId),
)
}.mapLeft {
OnrampError.UnknownError
}
}.mapLeft(errorResolver::resolve)
}
}

View file

@ -3,18 +3,19 @@ package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEmpty
class GetOnrampTransactionsUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
private val errorResolver: OnrampErrorResolver,
) {
operator fun invoke(
@ -25,7 +26,6 @@ class GetOnrampTransactionsUseCase(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrencyId,
).map { it.right() }
.catch { OnrampError.UnknownError.left() }
.onEmpty { OnrampError.UnknownError.left() }
.catch { errorResolver.resolve(it).left() }
}
}

View file

@ -1,16 +1,21 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.CryptoCurrency
class OnrampFetchPairsUseCase(private val repository: OnrampRepository) {
class OnrampFetchPairsUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(cryptoCurrency: CryptoCurrency): Either<Throwable, Unit> {
suspend operator fun invoke(cryptoCurrency: CryptoCurrency): Either<OnrampError, Unit> {
return Either.catch {
val country = requireNotNull(repository.getDefaultCountrySync()) { "Country must not be null" }
val currency = requireNotNull(repository.getDefaultCurrencySync()) { "Currency must not be null" }
repository.fetchPairs(currency = currency, country = country, cryptoCurrency = cryptoCurrency)
}
}.mapLeft(errorResolver::resolve)
}
}

View file

@ -1,15 +1,20 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.CryptoCurrency
class OnrampFetchQuotesUseCase(private val repository: OnrampRepository) {
class OnrampFetchQuotesUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(amount: Amount, cryptoCurrency: CryptoCurrency) {
suspend operator fun invoke(amount: Amount, cryptoCurrency: CryptoCurrency) = Either.catch {
repository.fetchQuotes(
cryptoCurrency = cryptoCurrency,
amount = amount,
)
}
}.mapLeft(errorResolver::resolve)
}

View file

@ -2,20 +2,20 @@ package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.left
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
class OnrampRemoveTransactionUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(externalTxId: String?): Either<OnrampError, Unit> {
if (externalTxId == null) return OnrampError.UnknownError.left()
if (externalTxId == null) return OnrampError.DomainError("Transaction id not provided").left()
return Either.catch {
onrampTransactionRepository.removeTransaction(externalTxId)
}.mapLeft {
OnrampError.UnknownError
}
}.mapLeft(errorResolver::resolve)
}
}

View file

@ -1,12 +1,17 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class OnrampSaveDefaultCountryUseCase(private val repository: OnrampRepository) {
class OnrampSaveDefaultCountryUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(country: OnrampCountry) {
suspend operator fun invoke(country: OnrampCountry) = Either.catch {
repository.saveDefaultCurrency(country.defaultCurrency)
repository.saveDefaultCountry(country)
}
}.mapLeft(errorResolver::resolve)
}

View file

@ -1,11 +1,16 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class OnrampSaveDefaultCurrencyUseCase(private val repository: OnrampRepository) {
class OnrampSaveDefaultCurrencyUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(currency: OnrampCurrency) {
suspend operator fun invoke(currency: OnrampCurrency) = Either.catch {
repository.saveDefaultCurrency(currency)
}
}.mapLeft(errorResolver::resolve)
}

View file

@ -2,11 +2,17 @@ package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class OnrampSaveSelectedPaymentMethod(private val repository: OnrampRepository) {
class OnrampSaveSelectedPaymentMethod(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(paymentMethod: OnrampPaymentMethod): Either<Throwable, Unit> {
suspend operator fun invoke(paymentMethod: OnrampPaymentMethod): Either<OnrampError, Unit> {
return Either.catch { repository.saveSelectedPaymentMethod(paymentMethod) }
.mapLeft(errorResolver::resolve)
}
}

View file

@ -1,16 +1,18 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
class OnrampSaveTransactionUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(transaction: OnrampTransaction): Either<OnrampError, Unit> {
return Either.catch { onrampTransactionRepository.storeTransaction(transaction) }
.mapLeft { OnrampError.UnknownError }
.mapLeft(errorResolver::resolve)
}
}

View file

@ -2,13 +2,15 @@ package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
class OnrampUpdateTransactionStatusUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(externalTxId: String, status: OnrampStatus.Status) = Either.catch {
onrampTransactionRepository.updateTransactionStatus(externalTxId = externalTxId, status = status)
}
}.mapLeft(errorResolver::resolve)
}

View file

@ -1,6 +1,6 @@
package com.tangem.domain.onramp.repositories
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.error.OnrampError
interface OnrampErrorResolver {

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.onramp.model.OnrampCurrency
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.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.*
@ -162,35 +163,23 @@ internal class OnrampAmountStateFactory(
private fun OnrampQuote.Error.toSecondaryFieldUiModel(
amountState: OnrampAmountBlockUM,
): OnrampAmountSecondaryFieldUM.Error {
return when (this) {
is OnrampQuote.Error.AmountTooBigError -> {
val amount = requiredAmount.value.format {
fiat(
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol,
)
}
OnrampAmountSecondaryFieldUM.Error(
resourceReference(
R.string.onramp_max_amount_restriction,
wrappedList(amount),
),
)
}
is OnrampQuote.Error.AmountTooSmallError -> {
val amount = requiredAmount.value.format {
fiat(
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol,
)
}
OnrampAmountSecondaryFieldUM.Error(
resourceReference(
R.string.onramp_min_amount_restriction,
wrappedList(amount),
),
)
}
val amount = error.requiredAmount.format {
fiat(
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol,
)
}
val errorTextRes = when (error) {
is OnrampError.AmountError.TooBigError -> R.string.onramp_max_amount_restriction
is OnrampError.AmountError.TooSmallError -> R.string.onramp_min_amount_restriction
}
return OnrampAmountSecondaryFieldUM.Error(
resourceReference(
errorTextRes,
wrappedList(amount),
),
)
}
}

View file

@ -113,7 +113,7 @@ internal class OnrampMainComponentModel @Inject constructor(
modelScope.launch {
checkOnrampAvailabilityUseCase.invoke()
.onRight(::handleOnrampAvailability)
.onLeft { Timber.e(it) }
.onLeft { Timber.e(it.toString()) }
}
}

View file

@ -20,15 +20,12 @@ import com.tangem.domain.onramp.OnrampSaveSelectedPaymentMethod
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM
import com.tangem.features.onramp.providers.SelectProviderComponent
import com.tangem.features.onramp.providers.entity.ProviderListBottomSheetConfig
import com.tangem.features.onramp.providers.entity.ProviderListItemUM
import com.tangem.features.onramp.providers.entity.ProviderListPaymentMethodUM
import com.tangem.features.onramp.providers.entity.ProviderListUM
import com.tangem.utils.StringsSigns.MINUS
import com.tangem.features.onramp.providers.entity.*
import com.tangem.utils.StringsSigns.MINUS
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -79,7 +76,7 @@ internal class SelectProviderModel @Inject constructor(
)
}
}
.onLeft { Timber.e(it) }
.onLeft { Timber.e(it.toString()) }
}
}
@ -171,40 +168,25 @@ internal class SelectProviderModel @Inject constructor(
},
)
}
is OnrampProviderWithQuote.Unavailable.AvailableFrom -> {
val amount = quote.requiredAmount.value.format {
crypto(symbol = quote.requiredAmount.symbol, decimals = quote.requiredAmount.decimals)
is OnrampProviderWithQuote.Unavailable.Error -> {
val quoteError = quote.quoteError
val amount = quoteError.error.requiredAmount.format {
crypto(symbol = quoteError.fromAmount.symbol, decimals = quoteError.fromAmount.decimals)
}
val errorSubtitleRes = when (quoteError.error) {
is OnrampError.AmountError.TooBigError -> R.string.express_provider_max_amount
is OnrampError.AmountError.TooSmallError -> R.string.express_provider_min_amount
}
ProviderListItemUM.AvailableWithError(
providerId = quote.provider.id,
imageUrl = quote.provider.info.imageLarge,
name = quote.provider.info.name,
subtitle = resourceReference(R.string.express_provider_min_amount, wrappedList(amount)),
subtitle = resourceReference(errorSubtitleRes, wrappedList(amount)),
onClick = {
onProviderSelected(
result = SelectProviderResult.ProviderWithError(
paymentMethod = quote.paymentMethod,
provider = quote.provider,
quoteError = quote.quoteError,
),
isBestRate = bestProvider == quote,
)
},
)
}
is OnrampProviderWithQuote.Unavailable.AvailableUpTo -> {
val amount = quote.requiredAmount.value.format {
crypto(symbol = quote.requiredAmount.symbol, decimals = quote.requiredAmount.decimals)
}
ProviderListItemUM.AvailableWithError(
providerId = quote.provider.id,
imageUrl = quote.provider.info.imageLarge,
name = quote.provider.info.name,
subtitle = resourceReference(R.string.express_provider_max_amount, wrappedList(amount)),
onClick = {
onProviderSelected(
result = SelectProviderResult.ProviderWithError(
paymentMethod = quote.paymentMethod,
paymentMethod = quoteError.paymentMethod,
provider = quote.provider,
quoteError = quote.quoteError,
),
@ -250,8 +232,13 @@ internal class SelectProviderModel @Inject constructor(
is OnrampProviderWithQuote.Data -> it.toAmount.value
// negative difference to sort both when data and unavailable is present
is OnrampProviderWithQuote.Unavailable.AvailableFrom -> it.fromAmount.value - it.requiredAmount.value
is OnrampProviderWithQuote.Unavailable.AvailableUpTo -> it.requiredAmount.value - it.fromAmount.value
is OnrampProviderWithQuote.Unavailable.Error -> {
when (val error = it.quoteError.error) {
is OnrampError.AmountError.TooSmallError -> it.quoteError.fromAmount.value - error.requiredAmount
is OnrampError.AmountError.TooBigError -> error.requiredAmount - it.quoteError.fromAmount.value
else -> null
}
}
is OnrampProviderWithQuote.Unavailable.NotSupportedPaymentMethod -> null
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.ContentMessage
import com.tangem.domain.onramp.GetOnrampRedirectUrlUseCase
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.redirect.OnrampRedirectComponent
import com.tangem.features.onramp.redirect.entity.OnrampRedirectTopBarUM
@ -76,8 +77,8 @@ internal class OnrampRedirectModel @Inject constructor(
}
}
private fun handleError(throwable: Throwable) {
Timber.e(throwable)
private fun handleError(error: OnrampError) {
Timber.e(error.toString())
val contentMessage = ContentMessage { onDismiss ->
BasicDialog(

View file

@ -2,13 +2,14 @@ package com.tangem.features.onramp.selectcountry.entity.transformer
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.selectcountry.entity.CountryItemState
import com.tangem.features.onramp.selectcountry.entity.CountryListUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toImmutableList
internal class UpdateCountryItemsTransformer(
private val maybeCountries: Either<Throwable, List<OnrampCountry>>,
private val maybeCountries: Either<OnrampError, List<OnrampCountry>>,
private val defaultCountry: OnrampCountry?,
private val query: String,
private val onRetry: () -> Unit,

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.onramp.model.OnrampCurrencies
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM
import com.tangem.features.onramp.selectcurrency.entity.CurrenciesSection
import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState
@ -11,7 +12,7 @@ import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toImmutableList
internal class UpdateCurrencyItemsTransformer(
private val maybeCurrencies: Either<Throwable, OnrampCurrencies>,
private val maybeCurrencies: Either<OnrampError, OnrampCurrencies>,
private val query: String,
private val onRetry: () -> Unit,
private val onCurrencyClick: (OnrampCurrency) -> Unit,

View file

@ -10,6 +10,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.onramp.GetOnrampCountryUseCase
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.settings.OnrampSettingsComponent
import com.tangem.features.onramp.settings.entity.OnrampSettingsConfig
import com.tangem.features.onramp.settings.entity.OnrampSettingsItemUM
@ -44,7 +45,7 @@ internal class OnrampSettingsModel @Inject constructor(
.launchIn(modelScope)
}
private fun updateResidenceState(maybeCountry: Either<Throwable, OnrampCountry?>) {
private fun updateResidenceState(maybeCountry: Either<OnrampError, OnrampCountry?>) {
maybeCountry.onRight { country ->
_state.update { state ->
state.copy(