Updated on 2026-08-14
This commit is contained in:
commit
1f6cbc7ed0
537 changed files with 6377 additions and 12897 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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?.invoke()
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.GetCardInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
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 sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
swapInteractorFactory: SwapInteractor.Factory,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
|
|
@ -84,9 +95,13 @@ internal class SwapViewModel @Inject constructor(
|
|||
?.unbundle(UserWalletId.serializer())
|
||||
?: error("no expected parameter UserWalletId found")
|
||||
|
||||
private val isInitiallyReversed: Boolean = savedStateHandle.get<Boolean>(AppRoute.Swap.IS_INITIAL_REVERSE_ORDER)
|
||||
?: false
|
||||
|
||||
private val swapInteractor = swapInteractorFactory.create(userWalletId)
|
||||
|
||||
private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
private var userWallet: UserWallet by Delegates.notNull()
|
||||
|
||||
private var isBalanceHidden = true
|
||||
|
||||
|
|
@ -120,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()
|
||||
|
|
@ -135,11 +153,13 @@ 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()
|
||||
initTokens(isInitiallyReversed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -182,18 +202,21 @@ internal class SwapViewModel @Inject constructor(
|
|||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(availableTokens = isAnyAvailableTokens))
|
||||
}
|
||||
|
||||
private fun initTokens() {
|
||||
private fun initTokens(isReverseFromTo: Boolean) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching(dispatchers.io) {
|
||||
swapInteractor.getTokensDataState(initialCryptoCurrency)
|
||||
}.onSuccess { state ->
|
||||
updateTokensState(state)
|
||||
val selectedCurrency = swapInteractor.getInitialCurrencyToSwap(
|
||||
initialCryptoCurrency = initialCryptoCurrency,
|
||||
state = state,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
applyInitialTokenChoice(
|
||||
state,
|
||||
swapInteractor.selectInitialCurrencyToSwap(
|
||||
initialCryptoCurrency,
|
||||
state,
|
||||
),
|
||||
state = state,
|
||||
selectedCurrency = selectedCurrency,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
|
||||
(dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let {
|
||||
|
|
@ -217,11 +240,12 @@ internal class SwapViewModel @Inject constructor(
|
|||
applyInitialTokenChoice(
|
||||
state = TokensDataStateExpress.EMPTY,
|
||||
selectedCurrency = null,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
|
||||
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,
|
||||
|
|
@ -229,33 +253,43 @@ internal class SwapViewModel @Inject constructor(
|
|||
initialCryptoCurrency.network.backendId,
|
||||
),
|
||||
)
|
||||
initTokens()
|
||||
initTokens(isReverseFromTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyInitialTokenChoice(state: TokensDataStateExpress, selectedCurrency: CryptoCurrencyStatus?) {
|
||||
val fromCurrencyStatus = initialCryptoCurrencyStatus
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = fromCurrencyStatus,
|
||||
toCryptoCurrency = selectedCurrency,
|
||||
tokensDataState = state,
|
||||
)
|
||||
private fun applyInitialTokenChoice(
|
||||
state: TokensDataStateExpress,
|
||||
selectedCurrency: CryptoCurrencyStatus?,
|
||||
isReverseFromTo: Boolean,
|
||||
) {
|
||||
// exceptional case
|
||||
if (selectedCurrency == null) {
|
||||
analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap)
|
||||
uiState = stateBuilder.createNoAvailableTokensToSwapState(
|
||||
uiStateHolder = uiState,
|
||||
fromToken = fromCurrencyStatus,
|
||||
)
|
||||
} else {
|
||||
startLoadingQuotes(
|
||||
fromToken = fromCurrencyStatus,
|
||||
toToken = selectedCurrency,
|
||||
amount = lastAmount.value,
|
||||
toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency),
|
||||
fromToken = initialCryptoCurrencyStatus,
|
||||
)
|
||||
return
|
||||
}
|
||||
isOrderReversed = isReverseFromTo
|
||||
val (fromCurrencyStatus, toCurrencyStatus) = if (isOrderReversed) {
|
||||
selectedCurrency to initialCryptoCurrencyStatus
|
||||
} else {
|
||||
initialCryptoCurrencyStatus to selectedCurrency
|
||||
}
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = fromCurrencyStatus,
|
||||
toCryptoCurrency = toCurrencyStatus,
|
||||
tokensDataState = state,
|
||||
)
|
||||
startLoadingQuotes(
|
||||
fromToken = fromCurrencyStatus,
|
||||
toToken = toCurrencyStatus,
|
||||
amount = lastAmount.value,
|
||||
toProvidersList = findSwapProviders(fromCurrencyStatus, toCurrencyStatus),
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateTokensState(tokenDataState: TokensDataStateExpress) {
|
||||
|
|
@ -399,7 +433,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(),
|
||||
)
|
||||
|
|
@ -408,7 +442,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}"
|
||||
}
|
||||
|
|
@ -569,20 +603,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 {
|
||||
|
|
@ -653,23 +686,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
|
||||
|
|
@ -889,15 +927,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")
|
||||
|
|
@ -1139,7 +1173,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 {
|
||||
|
|
@ -1227,6 +1262,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(),
|
||||
)
|
||||
|
||||
sendFeedbackEmailUseCase(email)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val INITIAL_AMOUNT = ""
|
||||
const val UPDATE_DELAY = 10000L
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue