Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-27 19:30:50 +05:00
commit 18af16b795
25 changed files with 502 additions and 289 deletions

View file

@ -95,6 +95,11 @@ internal class FeedbackDataBuilder {
)
}
fun addSwapInfo(providerName: String, txId: String) {
builder.appendKeyValue("Provider", providerName)
builder.appendKeyValue("Transaction ID", txId)
}
fun addDelimiter(): StringBuilder = builder.appendDelimiter()
fun build(): String = builder.trimEnd().toString()

View file

@ -30,4 +30,10 @@ sealed interface FeedbackEmailType {
val transactionTypes: List<String>,
val unsignedTransactions: List<String?>,
) : FeedbackEmailType
data class SwapProblem(
override val cardInfo: CardInfo,
val providerName: String,
val txId: String,
) : FeedbackEmailType
}

View file

@ -29,6 +29,7 @@ internal class EmailMessageBodyResolver(
type.transactionTypes,
type.unsignedTransactions,
)
is FeedbackEmailType.SwapProblem -> addSwapProblemBody(type.cardInfo, type.providerName, type.txId)
}
return build()
@ -108,6 +109,35 @@ internal class EmailMessageBodyResolver(
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private suspend fun FeedbackDataBuilder.addSwapProblemBody(
cardInfo: CardInfo,
providerName: String,
txId: String,
) {
addCardInfo(cardInfo)
addDelimiter()
val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
feedbackRepository.getBlockchainInfo(
userWalletId = userWalletId,
blockchainId = blockchainError.blockchainId,
derivationPath = blockchainError.derivationPath,
)
}
if (blockchainInfo != null) {
addBlockchainError(blockchainInfo, blockchainError)
addDelimiter()
}
addSwapInfo(providerName, txId)
addDelimiter()
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) {
addCardInfo(cardInfo)
addDelimiter()

View file

@ -21,6 +21,7 @@ internal class EmailMessageTitleResolver(private val resources: Resources) {
is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed
is FeedbackEmailType.TransactionSendingProblem,
is FeedbackEmailType.StakingProblem,
is FeedbackEmailType.SwapProblem,
-> R.string.feedback_preface_tx_failed
}
.let(resources::getString)

View file

@ -25,8 +25,10 @@ internal class EmailSubjectResolver(private val resources: Resources) {
}
is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative
is FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed
is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed
is FeedbackEmailType.StakingProblem -> R.string.feedback_subject_tx_failed
is FeedbackEmailType.TransactionSendingProblem,
is FeedbackEmailType.StakingProblem,
is FeedbackEmailType.SwapProblem,
-> R.string.feedback_subject_tx_failed
}
.let(resources::getString)
}

View file

@ -50,29 +50,17 @@ internal fun StakingEventEffect(event: StateEvent<StakingEvent>, snackbarHostSta
@Composable
internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) {
val confirmButton: DialogButtonUM
val dismissButton: DialogButtonUM?
val onActionClick = state.onConfirmClick
if (onActionClick != null) {
confirmButton = DialogButtonUM(
title = state.confirmButtonText.resolveReference(),
onClick = {
onActionClick()
onDismiss()
},
)
dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel),
onClick = onDismiss,
)
} else {
confirmButton = DialogButtonUM(
title = state.confirmButtonText.resolveReference(),
onClick = onDismiss,
)
dismissButton = null
}
val confirmButton = DialogButtonUM(
title = state.confirmButtonText.resolveReference(),
onClick = {
state.onConfirmClick()
onDismiss()
},
)
val dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel),
onClick = onDismiss,
)
BasicDialog(
message = state.message.resolveReference(),

View file

@ -27,7 +27,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.converters.*
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.ExpressException
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.feature.swap.domain.models.domain.*
@ -196,7 +196,7 @@ internal class DefaultSwapRepository @Inject constructor(
toDecimals: Int,
providerId: String,
rateType: RateType,
): Either<DataError, QuoteModel> {
): Either<ExpressDataError, QuoteModel> {
return withContext(coroutineDispatcher.io) {
try {
val response = tangemExpressApi.getExchangeQuote(
@ -234,7 +234,7 @@ internal class DefaultSwapRepository @Inject constructor(
toAddress: String,
refundAddress: String?, // for cex only
refundExtraId: String?, // for cex only
): Either<DataError, SwapDataModel> {
): Either<ExpressDataError, SwapDataModel> {
return withContext(coroutineDispatcher.io) {
try {
val requestId = UUID.randomUUID().toString()
@ -256,12 +256,12 @@ internal class DefaultSwapRepository @Inject constructor(
).getOrThrow()
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {
val txDetails = parseTxDetails(response.txDetailsJson)
?: return@withContext DataError.UnknownError.left()
?: return@withContext ExpressDataError.UnknownError.left()
if (txDetails.requestId != requestId) {
return@withContext DataError.InvalidRequestIdError().left()
return@withContext ExpressDataError.InvalidRequestIdError().left()
}
if (!toAddress.equals(txDetails.payoutAddress, ignoreCase = true)) {
return@withContext DataError.InvalidPayoutAddressError().left()
return@withContext ExpressDataError.InvalidPayoutAddressError().left()
}
expressDataConverter.convert(
ExchangeDataResponseWithTxDetails(
@ -270,7 +270,7 @@ internal class DefaultSwapRepository @Inject constructor(
),
).right()
} else {
DataError.InvalidSignatureError().left()
ExpressDataError.InvalidSignatureError().left()
}
} catch (ex: Exception) {
getDataError(ex).left()
@ -285,7 +285,7 @@ internal class DefaultSwapRepository @Inject constructor(
payInAddress: String,
txHash: String,
payInExtraId: String?,
): Either<DataError, Unit> = withContext(coroutineDispatcher.io) {
): Either<ExpressDataError, Unit> = withContext(coroutineDispatcher.io) {
try {
tangemExpressApi.exchangeSent(
ExchangeSentRequestBody(
@ -400,11 +400,11 @@ internal class DefaultSwapRepository @Inject constructor(
)
}
private fun getDataError(ex: Exception): DataError {
private fun getDataError(ex: Exception): ExpressDataError {
return if (ex is ApiResponseError.HttpException) {
errorsDataConverter.convert(ex.errorBody ?: "")
} else {
DataError.UnknownError
ExpressDataError.UnknownError
}
}
}

View file

@ -3,76 +3,77 @@ package com.tangem.feature.swap.converters
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.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.utils.converter.Converter
internal class ErrorsDataConverter(
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
) : Converter<String, DataError> {
) : Converter<String, ExpressDataError> {
@Suppress("MagicNumber", "CyclomaticComplexMethod")
override fun convert(value: String): DataError {
override fun convert(value: String): ExpressDataError {
try {
val error = jsonAdapter.fromJson(value)?.error ?: return DataError.UnknownError
val error = jsonAdapter.fromJson(value)?.error ?: return ExpressDataError.UnknownError
return when (error.code) {
2010 -> DataError.BadRequest(code = error.code)
2200 -> DataError.SwapsAreUnavailableNowError(code = error.code)
2210 -> DataError.ExchangeProviderNotFoundError(code = error.code)
2220 -> DataError.ExchangeProviderNotActiveError(code = error.code)
2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code)
2231 -> DataError.ExchangeProviderProviderInternalError(code = error.code)
2240 -> DataError.ExchangeNotPossibleError(code = error.code)
2010 -> ExpressDataError.BadRequest(code = error.code)
2200 -> ExpressDataError.SwapsAreUnavailableNowError(code = error.code)
2210 -> ExpressDataError.ExchangeProviderNotFoundError(code = error.code)
2220 -> ExpressDataError.ExchangeProviderNotActiveError(code = error.code)
2230 -> ExpressDataError.ExchangeProviderNotAvailableError(code = error.code)
2231 -> ExpressDataError.ExchangeProviderProviderInternalError(code = error.code)
2240 -> ExpressDataError.ExchangeNotPossibleError(code = error.code)
2250 -> tryParseExchangeTooSmallAmountError(error = error)
2251 -> tryParseExchangeTooBigAmountError(error = error)
2260 -> tryParseExchangeNotEnoughAllowanceError(error = error)
2270 -> DataError.ExchangeNotEnoughBalanceError(code = error.code)
2280 -> DataError.ExchangeInvalidAddressError(code = error.code)
2270 -> ExpressDataError.ExchangeNotEnoughBalanceError(code = error.code)
2280 -> ExpressDataError.ExchangeInvalidAddressError(code = error.code)
2290 -> tryParseExchangeInvalidFromDecimalsError(error = error)
else -> DataError.UnknownErrorWithCode(error.code)
else -> ExpressDataError.UnknownErrorWithCode(error.code)
}
} catch (e: Exception) {
return DataError.UnknownError
return ExpressDataError.UnknownError
}
}
private fun tryParseExchangeTooSmallAmountError(error: ExpressError): DataError {
val minAmount = error.value?.minAmount ?: return DataError.UnknownErrorWithCode(error.code)
val decimals = error.value?.decimals ?: return DataError.UnknownErrorWithCode(error.code)
private fun tryParseExchangeTooSmallAmountError(error: ExpressError): ExpressDataError {
val minAmount = error.value?.minAmount ?: return ExpressDataError.UnknownErrorWithCode(error.code)
val decimals = error.value?.decimals ?: return ExpressDataError.UnknownErrorWithCode(error.code)
return DataError.ExchangeTooSmallAmountError(
return ExpressDataError.ExchangeTooSmallAmountError(
code = error.code,
amount = createFromAmountWithOffset(minAmount, decimals),
)
}
private fun tryParseExchangeTooBigAmountError(error: ExpressError): DataError {
val minAmount = error.value?.maxAmount ?: return DataError.UnknownErrorWithCode(error.code)
val decimals = error.value?.decimals ?: return DataError.UnknownErrorWithCode(error.code)
private fun tryParseExchangeTooBigAmountError(error: ExpressError): ExpressDataError {
val minAmount = error.value?.maxAmount ?: return ExpressDataError.UnknownErrorWithCode(error.code)
val decimals = error.value?.decimals ?: return ExpressDataError.UnknownErrorWithCode(error.code)
return DataError.ExchangeTooBigAmountError(
return ExpressDataError.ExchangeTooBigAmountError(
code = error.code,
amount = createFromAmountWithOffset(minAmount, decimals),
)
}
private fun tryParseExchangeNotEnoughAllowanceError(error: ExpressError): DataError {
val currentAllowance = error.value?.currentAllowance ?: return DataError.UnknownErrorWithCode(error.code)
private fun tryParseExchangeNotEnoughAllowanceError(error: ExpressError): ExpressDataError {
val currentAllowance = error.value?.currentAllowance ?: return ExpressDataError.UnknownErrorWithCode(error.code)
return DataError.ExchangeNotEnoughAllowanceError(
return ExpressDataError.ExchangeNotEnoughAllowanceError(
code = error.code,
currentAllowance = currentAllowance,
)
}
private fun tryParseExchangeInvalidFromDecimalsError(error: ExpressError): DataError {
val receivedFromDecimals = error.value?.receivedFromDecimals ?: return DataError.UnknownErrorWithCode(
private fun tryParseExchangeInvalidFromDecimalsError(error: ExpressError): ExpressDataError {
val receivedFromDecimals = error.value?.receivedFromDecimals ?: return ExpressDataError.UnknownErrorWithCode(
code = error.code,
)
val expressFromDecimals = error.value?.expressFromDecimals ?: return DataError.UnknownErrorWithCode(error.code)
val expressFromDecimals =
error.value?.expressFromDecimals ?: return ExpressDataError.UnknownErrorWithCode(error.code)
return DataError.ExchangeInvalidFromDecimalsError(
return ExpressDataError.ExchangeInvalidFromDecimalsError(
code = error.code,
receivedFromDecimals = receivedFromDecimals,
expressFromDecimals = expressFromDecimals,

View file

@ -3,7 +3,7 @@ package com.tangem.feature.swap.domain.api
import arrow.core.Either
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.domain.*
import java.math.BigDecimal
@ -27,7 +27,7 @@ interface SwapRepository {
toDecimals: Int,
providerId: String,
rateType: RateType,
): Either<DataError, QuoteModel>
): Either<ExpressDataError, QuoteModel>
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
@ -66,7 +66,7 @@ interface SwapRepository {
toAddress: String,
refundAddress: String? = null, // for cex only
refundExtraId: String? = null, // for cex only
): Either<DataError, SwapDataModel>
): Either<ExpressDataError, SwapDataModel>
// TODO: Add target error handling, remove either ([REDACTED_JIRA])
@Suppress("LongParameterList")
@ -77,7 +77,7 @@ interface SwapRepository {
payInAddress: String,
txHash: String,
payInExtraId: String?,
): Either<DataError, Unit>
): Either<ExpressDataError, Unit>
fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
}

View file

@ -12,6 +12,7 @@ android {
dependencies {
/** Domain */
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction.models)
/** Core modules */
implementation(projects.core.utils)

View file

@ -2,49 +2,52 @@ package com.tangem.feature.swap.domain.models
import java.math.BigDecimal
sealed class DataError {
sealed class ExpressDataError {
abstract val code: Int
data class BadRequest(override val code: Int) : DataError()
data class BadRequest(override val code: Int) : ExpressDataError()
data class SwapsAreUnavailableNowError(override val code: Int) : DataError()
data class SwapsAreUnavailableNowError(override val code: Int) : ExpressDataError()
data class ExchangeProviderNotFoundError(override val code: Int) : DataError()
data class ExchangeProviderNotFoundError(override val code: Int) : ExpressDataError()
data class ExchangeProviderNotActiveError(override val code: Int) : DataError()
data class ExchangeProviderNotActiveError(override val code: Int) : ExpressDataError()
data class ExchangeProviderNotAvailableError(override val code: Int) : DataError()
data class ExchangeProviderNotAvailableError(override val code: Int) : ExpressDataError()
data class ExchangeProviderProviderInternalError(override val code: Int) : DataError()
data class ExchangeProviderProviderInternalError(override val code: Int) : ExpressDataError()
data class ExchangeNotPossibleError(override val code: Int) : DataError()
data class ExchangeNotPossibleError(override val code: Int) : ExpressDataError()
data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount) : DataError()
data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount) : ExpressDataError()
data class ExchangeTooBigAmountError(override val code: Int, val amount: SwapAmount) : DataError()
data class ExchangeTooBigAmountError(override val code: Int, val amount: SwapAmount) : ExpressDataError()
data class ExchangeNotEnoughAllowanceError(override val code: Int, val currentAllowance: BigDecimal) : DataError()
data class ExchangeNotEnoughAllowanceError(
override val code: Int,
val currentAllowance: BigDecimal,
) : ExpressDataError()
data class ExchangeNotEnoughBalanceError(override val code: Int) : DataError()
data class ExchangeNotEnoughBalanceError(override val code: Int) : ExpressDataError()
data class ExchangeInvalidAddressError(override val code: Int) : DataError()
data class ExchangeInvalidAddressError(override val code: Int) : ExpressDataError()
data class ExchangeInvalidFromDecimalsError(
override val code: Int,
val receivedFromDecimals: Int,
val expressFromDecimals: Int,
) : DataError()
) : ExpressDataError()
data class UnknownErrorWithCode(override val code: Int) : DataError()
data class UnknownErrorWithCode(override val code: Int) : ExpressDataError()
data class InvalidSignatureError(override val code: Int = 990) : DataError()
data class InvalidSignatureError(override val code: Int = 990) : ExpressDataError()
data class InvalidRequestIdError(override val code: Int = 991) : DataError()
data class InvalidRequestIdError(override val code: Int = 991) : ExpressDataError()
data class InvalidPayoutAddressError(override val code: Int = 992) : DataError()
data class InvalidPayoutAddressError(override val code: Int = 992) : ExpressDataError()
data object UnknownError : DataError() {
data object UnknownError : ExpressDataError() {
override val code: Int = -1
}
}

View file

@ -1,3 +1,3 @@
package com.tangem.feature.swap.domain.models
class ExpressException(val dataError: DataError) : Exception()
class ExpressException(val expressDataError: ExpressDataError) : Exception()

View file

@ -1,7 +1,7 @@
package com.tangem.feature.swap.domain.models.ui
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import java.math.BigDecimal
@ -34,7 +34,7 @@ sealed interface SwapState {
data class SwapError(
val fromTokenInfo: TokenSwapInfo,
val error: DataError,
val error: ExpressDataError,
val includeFeeInAmount: IncludeFeeInAmount,
) : SwapState
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.domain.models.ui
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.feature.swap.domain.models.ExpressDataError
import java.math.BigDecimal
sealed class SwapTransactionState {
@ -15,17 +16,13 @@ sealed class SwapTransactionState {
val timestamp: Long,
) : SwapTransactionState()
data object UserCancelled : SwapTransactionState()
data object BlockchainError : SwapTransactionState()
data object TangemSdkError : SwapTransactionState()
data object NetworkError : SwapTransactionState()
data object UnknownError : SwapTransactionState()
data class ExpressError(val dataError: DataError) : SwapTransactionState()
data object DemoMode : SwapTransactionState()
sealed class Error : SwapTransactionState() {
data class TransactionError(val error: SendTransactionError?) : Error()
data class ExpressError(val error: ExpressDataError) : Error()
data object UnknownError : Error()
}
}

View file

@ -22,7 +22,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.models.TransactionType
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.utils.convertToSdkAmount
@ -31,7 +30,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
@ -227,7 +226,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
),
).getOrElse {
Timber.e(it, "Failed to create approveTransaction")
return SwapTransactionState.UnknownError
return SwapTransactionState.Error.UnknownError
}
val result = sendTransactionUseCase(
@ -243,16 +242,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
timestamp = System.currentTimeMillis(),
)
},
ifLeft = {
when (it) {
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
else -> SwapTransactionState.UnknownError
}
},
ifLeft = { SwapTransactionState.Error.TransactionError(it) },
)
}
@ -591,7 +581,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
""".trimIndent(),
)
val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return SwapTransactionState.UnknownError
val cardId = userWallet.scanResponse.card.cardId
if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode
@ -693,12 +682,12 @@ internal class SwapInteractorImpl @AssistedInject constructor(
hash = dataToSign,
).getOrElse {
Timber.e(it, "Failed to create swap dex tx data")
return SwapTransactionState.UnknownError
return SwapTransactionState.Error.UnknownError
}
val result = sendTransactionUseCase(
txData = txData,
userWallet = getUserWalletUseCase(userWalletId).getOrElse { return SwapTransactionState.UnknownError },
userWallet = userWallet,
network = currencyToSendStatus.currency.network,
)
return result.fold(
@ -738,7 +727,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
timestamp = System.currentTimeMillis(),
)
},
ifLeft = { handleSendTxError(it) },
ifLeft = { SwapTransactionState.Error.TransactionError(it) },
)
}
@ -773,14 +762,14 @@ internal class SwapInteractorImpl @AssistedInject constructor(
toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty(),
refundAddress = currencyToSend.value.networkAddress?.defaultAddress?.value,
refundExtraId = null, // currently always null
).getOrElse { return SwapTransactionState.ExpressError(it) }
).getOrElse { return SwapTransactionState.Error.ExpressError(it) }
val exchangeDataCex =
exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.UnknownError
exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError
val cardId = userWallet.scanResponse.card.cardId
if (isDemoCardUseCase(cardId)) return SwapTransactionState.UnknownError
if (isDemoCardUseCase(cardId)) return SwapTransactionState.Error.UnknownError
val txData = createTransactionUseCase(
amount = amount.value.convertToSdkAmount(currencyToSend.currency),
@ -794,11 +783,11 @@ internal class SwapInteractorImpl @AssistedInject constructor(
network = currencyToSend.currency.network,
).getOrElse {
Timber.e(it, "Failed to create swap CEX tx data")
return SwapTransactionState.UnknownError
return SwapTransactionState.Error.UnknownError
}
if (txData.extras == null && exchangeDataCex.txExtraId != null) {
return SwapTransactionState.UnknownError
return SwapTransactionState.Error.UnknownError
}
val result = sendTransactionUseCase(
@ -809,9 +798,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val derivationPath = currencyToSend.currency.network.derivationPath.value
return result.fold(
ifLeft = {
handleSendTxError(it)
},
ifLeft = { SwapTransactionState.Error.TransactionError(it) },
ifRight = { txHash ->
repository.exchangeSent(
txId = exchangeDataCex.txId,
@ -856,17 +843,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
)
}
private fun handleSendTxError(txError: SendTransactionError?): SwapTransactionState {
return when (txError) {
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
else -> SwapTransactionState.UnknownError
}
}
private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee {
val feeAmountValue = fee.feeValue
val feeAmount = Amount(
@ -1081,7 +1057,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
@Suppress("LongMethod")
private suspend fun getQuotesState(
provider: SwapProvider,
quoteDataModel: Either<DataError, QuoteModel>,
quoteDataModel: Either<ExpressDataError, QuoteModel>,
amount: SwapAmount,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
@ -1160,7 +1136,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
fromToken = fromToken,
amount = amount,
includeFeeInAmount = includeFeeInAmount,
dataError = error,
expressDataError = error,
)
},
)
@ -1170,7 +1146,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
fromToken: CryptoCurrencyStatus,
amount: SwapAmount,
includeFeeInAmount: IncludeFeeInAmount,
dataError: DataError,
expressDataError: ExpressDataError,
): SwapState.SwapError {
val rates = getQuotes(fromToken.currency.id)
val fromTokenSwapInfo = TokenSwapInfo(
@ -1179,7 +1155,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
?: BigDecimal.ZERO,
cryptoCurrencyStatus = fromToken,
)
return SwapState.SwapError(fromTokenSwapInfo, dataError, includeFeeInAmount)
return SwapState.SwapError(fromTokenSwapInfo, expressDataError, includeFeeInAmount)
}
@Suppress("CyclomaticComplexMethod")
@ -1526,7 +1502,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
fromToken = fromTokenStatus,
amount = swapAmount,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
dataError = DataError.UnknownError,
expressDataError = ExpressDataError.UnknownError,
)
}
}

View file

@ -24,6 +24,7 @@ dependencies {
implementation(projects.core.decompose) // For Route supertype
/** Domain modules **/
implementation(projects.domain.models)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.balanceHiding)
@ -31,13 +32,17 @@ dependencies {
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction)
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
implementation(projects.domain.staking)
implementation(projects.domain.feedback)
/** Feature modules */
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
implementation(projects.domain.staking)
/** AndroidX */
implementation(deps.androidx.activity.compose)

View file

@ -0,0 +1,33 @@
package com.tangem.feature.swap.converters
import com.tangem.common.ui.alerts.TransactionErrorAlertConverter
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.feature.swap.domain.models.ui.SwapTransactionState
import com.tangem.feature.swap.models.SwapAlertUM
import com.tangem.feature.swap.utils.getExpressErrorMessage
import com.tangem.utils.converter.Converter
internal class SwapTransactionErrorStateConverter(
private val onDismiss: () -> Unit,
private val onSupportClick: (String) -> Unit,
) : Converter<SwapTransactionState.Error, AlertUM?> {
override fun convert(value: SwapTransactionState.Error): AlertUM? {
return when (value) {
is SwapTransactionState.Error.TransactionError -> {
when (val error = value.error) {
is SendTransactionError.UserCancelledError -> return null
null -> SwapAlertUM.GenericError(onDismiss)
else -> TransactionErrorAlertConverter(onDismiss, onSupportClick).convert(error)
}
}
is SwapTransactionState.Error.ExpressError -> {
SwapAlertUM.ExpressErrorAlert(
message = getExpressErrorMessage(value.error),
onConfirmClick = { onSupportClick(value.error.code.toString()) },
)
}
SwapTransactionState.Error.UnknownError -> SwapAlertUM.GenericError(onDismiss)
}
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.feature.swap.models
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
sealed class SwapAlertUM : AlertUM {
data class GenericError(
override val onConfirmClick: (() -> Unit),
override val message: TextReference = resourceReference(R.string.common_unknown_error),
) : SwapAlertUM() {
override val title: TextReference? = null
override val confirmButtonText: TextReference =
resourceReference(id = R.string.common_support)
}
data class ExpressErrorAlert(
override val message: TextReference = resourceReference(R.string.common_unknown_error),
override val onConfirmClick: (() -> Unit),
) : SwapAlertUM() {
override val title: TextReference? = null
override val confirmButtonText: TextReference =
resourceReference(id = R.string.common_support)
}
data class FeesAlert(
override val message: TextReference,
override val onConfirmClick: (() -> Unit),
) : SwapAlertUM() {
override val title: TextReference = resourceReference(
com.tangem.feature.swap.presentation.R.string.swapping_alert_title,
)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.common_ok)
}
}

View file

@ -6,18 +6,21 @@ import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.ui.PriceImpact
import com.tangem.feature.swap.models.states.FeeItemState
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.models.states.events.SwapEvent
data class SwapStateHolder(
internal data class SwapStateHolder(
val sendCardData: SwapCardState,
val receiveCardData: SwapCardState,
val blockchainId: String, // not the same as networkId, its local id in app
val warnings: List<SwapWarning> = emptyList(),
val alert: SwapWarning.GenericWarning? = null,
val event: StateEvent<SwapEvent> = consumedEvent(),
val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED,
val providerState: ProviderState,
@ -110,7 +113,6 @@ sealed interface SwapWarning {
data class GenericWarning(
val title: TextReference? = null,
val message: TextReference? = null,
val type: GenericWarningType = GenericWarningType.OTHER,
val onClick: () -> Unit,
) : SwapWarning
@ -133,10 +135,6 @@ sealed interface SwapWarning {
}
}
enum class GenericWarningType {
NETWORK, OTHER
}
enum class ChangeCardsButtonState {
ENABLED, DISABLED, UPDATE_IN_PROGRESS
}

View file

@ -0,0 +1,11 @@
package com.tangem.feature.swap.models.states.events
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.alerts.models.AlertUM
@Immutable
internal sealed class SwapEvent {
data class ShowAlert(val alert: AlertUM) : SwapEvent()
data class ShowShareDialog(val txUrl: String) : SwapEvent()
}

View file

@ -2,24 +2,31 @@ package com.tangem.feature.swap.ui
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
import com.tangem.common.ui.bottomsheet.permission.state.*
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter
import com.tangem.feature.swap.converters.TokensDataConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.*
import com.tangem.feature.swap.models.states.events.SwapEvent
import com.tangem.feature.swap.presentation.R
import com.tangem.feature.swap.utils.getExpressErrorMessage
import com.tangem.feature.swap.utils.getExpressErrorTitle
import com.tangem.feature.swap.viewmodels.SwapProcessDataState
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -625,13 +632,13 @@ internal class StateBuilder(
fromToken: TokenSwapInfo,
toToken: CryptoCurrencyStatus?,
includeFeeInAmount: IncludeFeeInAmount,
dataError: DataError,
expressDataError: ExpressDataError,
isReverseSwapPossible: Boolean,
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
val warnings = mutableListOf<SwapWarning>()
warnings.add(getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency))
warnings.add(getWarningForError(expressDataError, fromToken.cryptoCurrencyStatus.currency))
if (includeFeeInAmount is IncludeFeeInAmount.Included && uiStateHolder.fee is FeeItemState.Content) {
val feeCoverageNotification = createNetworkFeeCoverageNotificationConfig(
uiStateHolder.fee.amountCrypto,
@ -642,7 +649,7 @@ internal class StateBuilder(
val providerState = getProviderStateForError(
swapProvider = swapProvider,
fromToken = fromToken.cryptoCurrencyStatus.currency,
dataError = dataError,
expressDataError = expressDataError,
onProviderClick = actions.onProviderClick,
selectionType = ProviderState.SelectionType.CLICK,
)
@ -698,28 +705,28 @@ internal class StateBuilder(
private fun getProviderStateForError(
swapProvider: SwapProvider,
fromToken: CryptoCurrency,
dataError: DataError,
expressDataError: ExpressDataError,
onProviderClick: (String) -> Unit,
selectionType: ProviderState.SelectionType,
): ProviderState {
return when (dataError) {
is DataError.ExchangeTooSmallAmountError -> {
return when (expressDataError) {
is ExpressDataError.ExchangeTooSmallAmountError -> {
swapProvider.convertToAvailableFromProviderState(
swapProvider = swapProvider,
alertText = resourceReference(
R.string.express_provider_min_amount,
wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
),
selectionType = selectionType,
onProviderClick = onProviderClick,
)
}
is DataError.ExchangeTooBigAmountError -> {
is ExpressDataError.ExchangeTooBigAmountError -> {
swapProvider.convertToAvailableFromProviderState(
swapProvider = swapProvider,
alertText = resourceReference(
R.string.express_provider_max_amount,
wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
),
selectionType = selectionType,
onProviderClick = onProviderClick,
@ -731,25 +738,25 @@ internal class StateBuilder(
}
}
private fun getWarningForError(dataError: DataError, fromToken: CryptoCurrency): SwapWarning {
val providerErrorMessage = getProviderErrorMessage(dataError)
val providerErrorTitle = getProviderErrorTitle(dataError)
return when (dataError) {
is DataError.ExchangeTooSmallAmountError -> SwapWarning.GeneralError(
private fun getWarningForError(expressDataError: ExpressDataError, fromToken: CryptoCurrency): SwapWarning {
val providerErrorMessage = getExpressErrorMessage(expressDataError)
val providerErrorTitle = getExpressErrorTitle(expressDataError)
return when (expressDataError) {
is ExpressDataError.ExchangeTooSmallAmountError -> SwapWarning.GeneralError(
notificationConfig = NotificationConfig(
title = resourceReference(
id = R.string.warning_express_too_minimal_amount_title,
formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
formatArgs = wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
),
subtitle = resourceReference(R.string.warning_express_wrong_amount_description),
iconResId = R.drawable.ic_alert_circle_24,
),
)
is DataError.ExchangeTooBigAmountError -> SwapWarning.GeneralError(
is ExpressDataError.ExchangeTooBigAmountError -> SwapWarning.GeneralError(
notificationConfig = NotificationConfig(
title = resourceReference(
id = R.string.warning_express_too_maximum_amount_title,
formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
formatArgs = wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
),
subtitle = resourceReference(R.string.warning_express_wrong_amount_description),
iconResId = R.drawable.ic_alert_circle_24,
@ -1045,81 +1052,43 @@ internal class StateBuilder(
)
}
fun createErrorTransaction(
fun createErrorTransactionAlert(
uiState: SwapStateHolder,
swapTransactionState: SwapTransactionState,
onAlertClick: () -> Unit,
error: SwapTransactionState.Error,
onDismiss: () -> Unit,
onSupportClick: (String) -> Unit,
): SwapStateHolder {
val errorAlert = SwapTransactionErrorStateConverter(
onSupportClick = onSupportClick,
onDismiss = onDismiss,
).convert(error)
return uiState.copy(
alert = SwapWarning.GenericWarning(
message = if (swapTransactionState is SwapTransactionState.ExpressError) {
getProviderErrorMessage(swapTransactionState.dataError)
} else {
null
},
onClick = onAlertClick,
type = if (swapTransactionState is SwapTransactionState.NetworkError) {
GenericWarningType.NETWORK
} else {
GenericWarningType.OTHER
},
),
event = errorAlert?.let {
triggeredEvent(
data = SwapEvent.ShowAlert(errorAlert),
onConsume = onDismiss,
)
} ?: consumedEvent(),
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
)
}
fun createDemoModeAlert(uiState: SwapStateHolder, onAlertClick: () -> Unit): SwapStateHolder {
fun createDemoModeAlert(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder {
return uiState.copy(
alert = SwapWarning.GenericWarning(
title = resourceReference(id = R.string.warning_demo_mode_title),
message = resourceReference(id = R.string.warning_demo_mode_message),
onClick = onAlertClick,
type = GenericWarningType.OTHER,
event = triggeredEvent(
data = SwapEvent.ShowAlert(AlertDemoModeUM(onDismiss)),
onConsume = onDismiss,
),
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
)
}
private fun getProviderErrorMessage(dataError: DataError): TextReference {
return when (dataError) {
is DataError.SwapsAreUnavailableNowError -> resourceReference(
id = R.string.express_error_swap_unavailable,
formatArgs = wrappedList(dataError.code),
)
is DataError.ExchangeNotPossibleError -> resourceReference(
id = R.string.warning_express_pair_unavailable_message,
formatArgs = wrappedList(dataError.code),
)
is DataError.UnknownError -> resourceReference(R.string.common_unknown_error)
is DataError.ExchangeProviderNotActiveError,
is DataError.ExchangeProviderNotFoundError,
is DataError.ExchangeProviderNotAvailableError,
is DataError.ExchangeProviderProviderInternalError,
-> resourceReference(
id = R.string.express_error_swap_pair_unavailable,
formatArgs = wrappedList(dataError.code),
)
else -> resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString()))
}
}
private fun getProviderErrorTitle(dataError: DataError): TextReference {
return when (dataError) {
is DataError.ExchangeNotPossibleError -> resourceReference(
id = R.string.warning_express_pair_unavailable_title,
formatArgs = wrappedList(dataError.code),
)
is DataError.UnknownError -> resourceReference(R.string.common_error)
else -> resourceReference(R.string.warning_express_refresh_required_title)
}
}
fun createAlert(
uiState: SwapStateHolder,
isPriceImpact: Boolean,
token: String,
providerType: ExchangeProviderType,
onAlertClick: () -> Unit,
onDismiss: () -> Unit,
): SwapStateHolder {
val message = when (providerType) {
ExchangeProviderType.CEX -> resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))
@ -1136,29 +1105,38 @@ internal class StateBuilder(
}
}
return uiState.copy(
alert = SwapWarning.GenericWarning(
title = resourceReference(R.string.swapping_alert_title),
message = message,
onClick = onAlertClick,
type = GenericWarningType.OTHER,
event = triggeredEvent(
SwapEvent.ShowAlert(
SwapAlertUM.FeesAlert(
message = message,
onConfirmClick = onDismiss,
),
),
onConsume = onDismiss,
),
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
)
}
fun addAlert(uiState: SwapStateHolder, message: TextReference? = null, onClick: () -> Unit): SwapStateHolder {
fun addAlert(
uiState: SwapStateHolder,
message: TextReference = resourceReference(R.string.common_unknown_error),
onDismiss: () -> Unit = { clearAlert(uiState) },
): SwapStateHolder {
return uiState.copy(
alert = SwapWarning.GenericWarning(
message = message,
onClick = onClick,
event = triggeredEvent(
SwapEvent.ShowAlert(
SwapAlertUM.GenericError(onDismiss, message),
),
onConsume = onDismiss,
),
)
}
fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(alert = null)
fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(event = consumedEvent())
fun addWarning(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder {
val renewWarnings = uiState.warnings.filterNot { it is SwapWarning.GenericWarning }.toMutableList()
val renewWarnings = uiState.warnings.toMutableList()
renewWarnings.add(
SwapWarning.GenericWarning(
message = message,
@ -1421,7 +1399,7 @@ internal class StateBuilder(
is SwapState.SwapError -> getProviderStateForError(
swapProvider = provider,
fromToken = state.fromTokenInfo.cryptoCurrencyStatus.currency,
dataError = state.error,
expressDataError = state.error,
onProviderClick = onProviderSelect,
selectionType = ProviderState.SelectionType.SELECT,
)

View file

@ -0,0 +1,67 @@
package com.tangem.feature.swap.ui
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.shareText
import com.tangem.feature.swap.models.states.events.SwapEvent
import com.tangem.feature.swap.presentation.R
@Composable
internal fun SwapEventEffect(event: StateEvent<SwapEvent>) {
val context = LocalContext.current
var alertConfig by remember { mutableStateOf<AlertUM?>(value = null) }
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(key1 = alertConfig) {
keyboardController?.hide()
}
alertConfig?.let {
SwapAlert(state = it, onDismiss = { alertConfig = null })
}
EventEffect(
event = event,
onTrigger = { value ->
when (value) {
is SwapEvent.ShowAlert -> {
alertConfig = value.alert
}
is SwapEvent.ShowShareDialog -> {
context.shareText(value.txUrl)
}
}
},
)
}
@Composable
internal fun SwapAlert(state: AlertUM, onDismiss: () -> Unit) {
val confirmButton = DialogButtonUM(
title = state.confirmButtonText.resolveReference(),
onClick = {
state.onConfirmClick()
onDismiss()
},
)
val dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel),
onClick = onDismiss,
)
BasicDialog(
message = state.message.resolveReference(),
confirmButton = confirmButton,
onDismissDialog = onDismiss,
title = state.title?.resolveReference(),
dismissButton = dismissButton,
)
}

View file

@ -101,22 +101,9 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
)
}
if (state.alert != null) {
val message = if (state.alert.type == GenericWarningType.NETWORK) {
stringResource(id = R.string.disclaimer_error_loading)
} else {
state.alert.message?.resolveReference() ?: stringResource(id = R.string.common_unknown_error)
}
BasicDialog(
title = state.alert.title?.resolveReference(),
message = message,
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = state.alert.onClick,
),
onDismissDialog = state.alert.onClick,
)
}
SwapEventEffect(
event = state.event,
)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.feature.swap.utils
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.presentation.R
internal fun getExpressErrorMessage(expressDataError: ExpressDataError): TextReference {
return when (expressDataError) {
is ExpressDataError.SwapsAreUnavailableNowError -> resourceReference(
id = R.string.express_error_swap_unavailable,
formatArgs = wrappedList(expressDataError.code),
)
is ExpressDataError.ExchangeNotPossibleError -> resourceReference(
id = R.string.warning_express_pair_unavailable_message,
formatArgs = wrappedList(expressDataError.code),
)
is ExpressDataError.UnknownError -> resourceReference(R.string.common_unknown_error)
is ExpressDataError.ExchangeProviderNotActiveError,
is ExpressDataError.ExchangeProviderNotFoundError,
is ExpressDataError.ExchangeProviderNotAvailableError,
is ExpressDataError.ExchangeProviderProviderInternalError,
-> resourceReference(
id = R.string.express_error_swap_pair_unavailable,
formatArgs = wrappedList(expressDataError.code),
)
else -> resourceReference(R.string.express_error_code, wrappedList(expressDataError.code.toString()))
}
}
internal fun getExpressErrorTitle(expressDataError: ExpressDataError): TextReference {
return when (expressDataError) {
is ExpressDataError.ExchangeNotPossibleError -> resourceReference(
id = R.string.warning_express_pair_unavailable_title,
formatArgs = wrappedList(expressDataError.code),
)
is ExpressDataError.UnknownError -> resourceReference(R.string.common_error)
else -> resourceReference(R.string.warning_express_refresh_required_title)
}
}

View file

@ -19,17 +19,24 @@ import com.tangem.core.ui.utils.InputNumberFormatter
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.FeedbackManager
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.ExpressException
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
@ -64,7 +71,6 @@ typealias SuccessLoadedSwapData = Map<SwapProvider, SwapState.QuotesLoadedState>
@Suppress("LargeClass", "LongParameterList")
@HiltViewModel
internal class SwapViewModel @Inject constructor(
private val swapInteractorFactory: SwapInteractor.Factory,
private val blockchainInteractor: BlockchainInteractor,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
@ -73,6 +79,11 @@ internal class SwapViewModel @Inject constructor(
private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val feedbackManager: FeedbackManager,
swapInteractorFactory: SwapInteractor.Factory,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver {
@ -90,6 +101,7 @@ internal class SwapViewModel @Inject constructor(
private val swapInteractor = swapInteractorFactory.create(userWalletId)
private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus
private var userWallet: UserWallet by Delegates.notNull()
private var isBalanceHidden = true
@ -123,7 +135,10 @@ internal class SwapViewModel @Inject constructor(
private val isUserResolvableError: (SwapState) -> Boolean = {
it is SwapState.SwapError &&
(it.error is DataError.ExchangeTooSmallAmountError || it.error is DataError.ExchangeTooBigAmountError)
(
it.error is ExpressDataError.ExchangeTooSmallAmountError ||
it.error is ExpressDataError.ExchangeTooBigAmountError
)
}
private val fromTokenBalanceJobHolder = JobHolder()
@ -136,9 +151,11 @@ internal class SwapViewModel @Inject constructor(
viewModelScope.launch(dispatchers.io) {
val cryptoCurrencyStatus =
getCryptoCurrencyStatusUseCase(userWalletId, initialCryptoCurrency.id).getOrNull()
if (cryptoCurrencyStatus == null) {
uiState = stateBuilder.addAlert(uiState = uiState, onClick = swapRouter::back)
val wallet = getUserWalletUseCase(userWalletId).getOrNull()
if (cryptoCurrencyStatus == null || wallet == null) {
uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back)
} else {
userWallet = wallet
initialCryptoCurrencyStatus = cryptoCurrencyStatus
initTokens(isInitiallyReversed)
}
@ -226,7 +243,7 @@ internal class SwapViewModel @Inject constructor(
uiState = stateBuilder.createInitialErrorState(
uiState,
(it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code,
(it as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code,
) {
uiState = stateBuilder.createInitialLoadingState(
initialCurrency = initialCryptoCurrency,
@ -414,7 +431,7 @@ internal class SwapViewModel @Inject constructor(
swapProvider = provider,
fromToken = state.fromTokenInfo,
toToken = dataState.toCryptoCurrency,
dataError = state.error,
expressDataError = state.error,
includeFeeInAmount = state.includeFeeInAmount,
isReverseSwapPossible = isReverseSwapPossible(),
)
@ -423,7 +440,7 @@ internal class SwapViewModel @Inject constructor(
}
}
private fun sendErrorAnalyticsEvent(error: DataError, provider: SwapProvider) {
private fun sendErrorAnalyticsEvent(error: ExpressDataError, provider: SwapProvider) {
val receiveToken = dataState.toCryptoCurrency?.currency?.let {
"${it.network.backendId}:${it.symbol}"
}
@ -573,20 +590,19 @@ internal class SwapViewModel @Inject constructor(
swapRouter.openScreen(SwapNavScreen.Success)
}
is SwapTransactionState.UserCancelled -> {
startLoadingQuotesFromLastState()
}
is SwapTransactionState.DemoMode -> {
startLoadingQuotesFromLastState()
SwapTransactionState.DemoMode -> {
uiState = stateBuilder.createDemoModeAlert(uiState) {
uiState = stateBuilder.clearAlert(uiState)
}
}
else -> {
is SwapTransactionState.Error -> {
startLoadingQuotesFromLastState()
uiState = stateBuilder.createErrorTransaction(uiState, it) {
uiState = stateBuilder.clearAlert(uiState)
}
uiState = stateBuilder.createErrorTransactionAlert(
uiState = uiState,
error = it,
onDismiss = { uiState = stateBuilder.clearAlert(uiState) },
onSupportClick = ::onFailedTxEmailClick,
)
}
}
}.onFailure {
@ -657,23 +673,28 @@ internal class SwapViewModel @Inject constructor(
uiState = stateBuilder.dismissBottomSheet(uiState)
startLoadingQuotesFromLastState(isSilent = true)
}
is SwapTransactionState.UserCancelled -> Unit
else -> {
uiState = stateBuilder.createErrorTransaction(uiState, it) {
is SwapTransactionState.Error -> {
uiState = stateBuilder.createErrorTransactionAlert(
uiState = uiState,
error = it,
onDismiss = { uiState = stateBuilder.clearAlert(uiState) },
onSupportClick = ::onFailedTxEmailClick,
)
}
SwapTransactionState.DemoMode -> {
uiState = stateBuilder.createDemoModeAlert(uiState) {
uiState = stateBuilder.clearAlert(uiState)
}
}
}
}.onFailure { makeDefaultAlert() }
}.onFailure { showGenericError(it.message.orEmpty()) }
}.onFailure {
Timber.e(it.message.orEmpty())
makeDefaultAlert()
}
}
}
private fun showGenericError(message: String) {
makeDefaultAlert(resourceReference(R.string.common_unknown_error))
Timber.e(message)
}
private fun onSearchEntered(searchQuery: String) {
viewModelScope.launch(dispatchers.io) {
val tokenDataState = dataState.tokensDataState ?: return@launch
@ -880,15 +901,11 @@ internal class SwapViewModel @Inject constructor(
}
private fun makeDefaultAlert() {
uiState = stateBuilder.addAlert(uiState) {
uiState = stateBuilder.clearAlert(uiState)
}
uiState = stateBuilder.addAlert(uiState)
}
private fun makeDefaultAlert(message: TextReference) {
uiState = stateBuilder.addAlert(uiState, message) {
uiState = stateBuilder.clearAlert(uiState)
}
uiState = stateBuilder.addAlert(uiState, message)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -1130,7 +1147,8 @@ internal class SwapViewModel @Inject constructor(
toToken.currency.id.value
}
return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers ?: emptyList()
return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers
?: emptyList()
}
private fun Map<SwapProvider, SwapState>.getLastLoadedSuccessStates(): SuccessLoadedSwapData {
@ -1218,6 +1236,33 @@ internal class SwapViewModel @Inject constructor(
analyticsEventHandler.send(event = event)
}
private fun onFailedTxEmailClick(errorMessage: String) {
viewModelScope.launch {
val network = initialCryptoCurrencyStatus.currency.network
val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") }
saveBlockchainErrorUseCase(
error = BlockchainErrorInfo(
errorMessage = errorMessage,
blockchainId = network.id.value,
derivationPath = network.derivationPath.value,
destinationAddress = dataState.swapDataModel?.transaction?.txTo.orEmpty(),
tokenSymbol = initialCryptoCurrency.symbol,
amount = dataState.amount.orEmpty(),
fee = dataState.selectedFee?.feeCryptoFormatted.orEmpty(),
),
)
val email = FeedbackEmailType.SwapProblem(
cardInfo = cardInfo,
providerName = dataState.selectedProvider?.name.orEmpty(),
txId = dataState.swapDataModel?.transaction?.txId.orEmpty(),
)
feedbackManager.sendEmail(email)
}
}
private companion object {
const val INITIAL_AMOUNT = ""
const val UPDATE_DELAY = 10000L