Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-14 20:53:14 +04:00
commit 1f6cbc7ed0
537 changed files with 6377 additions and 12897 deletions

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

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