Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-25 20:31:05 +05:00
parent 7504fe1822
commit 321caf73a8
14 changed files with 357 additions and 192 deletions

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

@ -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

@ -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
@ -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(

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,15 +2,19 @@ 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.SwapAmount
@ -19,7 +23,10 @@ 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
@ -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,

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,13 +19,20 @@ 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
@ -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)
}
@ -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