Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-11 15:00:16 +04:00
parent 11b8f4f366
commit 9489035904
8 changed files with 104 additions and 53 deletions

View file

@ -219,16 +219,24 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Swap(
val currency: CryptoCurrency,
val currencyFrom: CryptoCurrency,
val currencyTo: CryptoCurrency? = null,
val userWalletId: UserWalletId,
val isInitialReverseOrder: Boolean = false,
) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}/$isInitialReverseOrder"),
) : AppRoute(
path = "/swap" +
"/${currencyFrom.id.value}" +
"/${currencyTo?.id?.value}" +
"/${userWalletId.stringValue}" +
"/$isInitialReverseOrder",
),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val CURRENCY_BUNDLE_KEY = "currency"
const val CURRENCY_FROM_KEY = "currencyFrom"
const val CURRENCY_TO_KEY = "currencyTo"
const val USER_WALLET_ID_KEY = "userWalletId"
const val IS_INITIAL_REVERSE_ORDER = "isInitialReverseOrder"
}

View file

@ -149,7 +149,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
router.push(
AppRoute.Swap(
currency = cryptoCurrencyData.status.currency,
currencyFrom = cryptoCurrencyData.status.currency,
userWalletId = cryptoCurrencyData.userWallet.walletId,
isInitialReverseOrder = true,
),

View file

@ -1,8 +1,12 @@
package com.tangem.features.onramp.swap.model
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.onramp.component.SwapSelectTokensComponent
import com.tangem.features.onramp.swap.entity.SwapSelectTokensController
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
import com.tangem.features.onramp.swap.entity.transformer.RemoveSelectedFromTokenTransformer
@ -15,8 +19,10 @@ import javax.inject.Inject
@Suppress("LongParameterList")
internal class SwapSelectTokensModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val controller: SwapSelectTokensController,
private val router: Router,
) : Model() {
val state: StateFlow<SwapSelectTokensUM> = controller.state
@ -26,6 +32,8 @@ internal class SwapSelectTokensModel @Inject constructor(
private val _fromCurrencyStatus = MutableStateFlow<CryptoCurrencyStatus?>(value = null)
private val _toCurrencyStatus = MutableStateFlow<CryptoCurrencyStatus?>(value = null)
private val params = paramsContainer.require<SwapSelectTokensComponent.Params>()
/**
* Select "from" token
*
@ -54,7 +62,13 @@ internal class SwapSelectTokensModel @Inject constructor(
controller.update(transformer = SelectToTokenTransformer(selectedTokenItemState))
// TODO: [REDACTED_JIRA]
router.push(
route = AppRoute.Swap(
currencyFrom = requireNotNull(fromCurrencyStatus.value).currency,
currencyTo = status.currency,
userWalletId = params.userWalletId,
),
)
}
private fun onRemoveClick() {

View file

@ -43,6 +43,7 @@ dependencies {
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
implementation(projects.features.wallet.api)
/** AndroidX */
implementation(deps.androidx.activity.compose)

View file

@ -63,9 +63,13 @@ internal class StateBuilder(
appCurrencyProvider = appCurrencyProvider,
)
fun createInitialLoadingState(initialCurrency: CryptoCurrency, networkInfo: NetworkInfo): SwapStateHolder {
fun createInitialLoadingState(
initialCurrencyFrom: CryptoCurrency,
initialCurrencyTo: CryptoCurrency?,
fromNetworkInfo: NetworkInfo,
): SwapStateHolder {
return SwapStateHolder(
blockchainId = networkInfo.blockchainId,
blockchainId = fromNetworkInfo.blockchainId,
sendCardData = SwapCardState.SwapCardData(
type = TransactionCardType.Inputtable(
onAmountChanged = actions.onAmountChanged,
@ -75,27 +79,27 @@ internal class StateBuilder(
amountEquivalent = null,
amountTextFieldValue = null,
token = null,
tokenIconUrl = initialCurrency.iconUrl,
tokenCurrency = initialCurrency.symbol,
coinId = initialCurrency.network.backendId,
tokenIconUrl = initialCurrencyFrom.iconUrl,
tokenCurrency = initialCurrencyFrom.symbol,
coinId = initialCurrencyFrom.network.backendId,
canSelectAnotherToken = false,
isNotNativeToken = initialCurrency is CryptoCurrency.Token,
isNotNativeToken = initialCurrencyFrom is CryptoCurrency.Token,
balance = "",
networkIconRes = getActiveIconRes(initialCurrency.network.id.value),
networkIconRes = getActiveIconRes(initialCurrencyFrom.network.id.value),
isBalanceHidden = true,
),
receiveCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReadOnly(),
amountEquivalent = null,
tokenIconUrl = "",
tokenCurrency = "",
tokenIconUrl = initialCurrencyTo?.iconUrl,
tokenCurrency = initialCurrencyTo?.symbol ?: "",
token = null,
amountTextFieldValue = null,
canSelectAnotherToken = false,
balance = "",
isNotNativeToken = false,
networkIconRes = null,
coinId = null,
isNotNativeToken = initialCurrencyTo is CryptoCurrency.Token,
networkIconRes = initialCurrencyTo?.let { getActiveIconRes(it.network.id.value) },
coinId = initialCurrencyTo?.network?.backendId,
isBalanceHidden = true,
),
fee = FeeItemState.Empty,

View file

@ -49,6 +49,7 @@ import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.router.SwapRouter
import com.tangem.feature.swap.ui.StateBuilder
import com.tangem.feature.swap.utils.formatToUIRepresentation
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import com.tangem.utils.isNullOrZero
@ -83,13 +84,26 @@ internal class SwapViewModel @Inject constructor(
private val getCardInfoUseCase: GetCardInfoUseCase,
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val walletFeatureToggles: WalletFeatureToggles,
swapInteractorFactory: SwapInteractor.Factory,
savedStateHandle: SavedStateHandle,
private val savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver {
private val initialCryptoCurrency: CryptoCurrency = savedStateHandle.get<Bundle>(AppRoute.Swap.CURRENCY_BUNDLE_KEY)
?.unbundle(CryptoCurrency.serializer())
?: error("no expected parameter CryptoCurrency found")
private val initialCurrencyFrom: CryptoCurrency
get() {
return savedStateHandle.get<Bundle>(AppRoute.Swap.CURRENCY_FROM_KEY)
?.unbundle(CryptoCurrency.serializer())
?: error("no expected parameter CryptoCurrency (from) found")
}
private val initialCurrencyTo: CryptoCurrency?
get() = if (walletFeatureToggles.isMainActionButtonsEnabled) {
savedStateHandle.get<Bundle>(AppRoute.Swap.CURRENCY_TO_KEY)
?.unbundle(CryptoCurrency.serializer())
?: error("no expected parameter CryptoCurrency (to) found")
} else {
null
}
private val userWalletId: UserWalletId = savedStateHandle.get<Bundle>(AppRoute.Swap.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
@ -100,7 +114,8 @@ internal class SwapViewModel @Inject constructor(
private val swapInteractor = swapInteractorFactory.create(userWalletId)
private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus
private lateinit var initialFromStatus: CryptoCurrencyStatus
private var initialToStatus: CryptoCurrencyStatus? = null
private var userWallet: UserWallet by Delegates.notNull()
private var isBalanceHidden = true
@ -122,8 +137,9 @@ internal class SwapViewModel @Inject constructor(
var uiState: SwapStateHolder by mutableStateOf(
stateBuilder.createInitialLoadingState(
initialCurrency = initialCryptoCurrency,
networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId),
initialCurrencyFrom = initialCurrencyFrom,
initialCurrencyTo = initialCurrencyTo,
fromNetworkInfo = blockchainInteractor.getBlockchainInfo(initialCurrencyFrom.network.backendId),
),
)
private set
@ -151,14 +167,16 @@ internal class SwapViewModel @Inject constructor(
init {
viewModelScope.launch(dispatchers.io) {
val cryptoCurrencyStatus =
getCryptoCurrencyStatusUseCase(userWalletId, initialCryptoCurrency.id).getOrNull()
val fromStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom.id).getOrNull()
val toStatus = initialCurrencyTo?.let { getCryptoCurrencyStatusUseCase(userWalletId, it.id).getOrNull() }
val wallet = getUserWalletUseCase(userWalletId).getOrNull()
if (cryptoCurrencyStatus == null || wallet == null) {
val isStatusToNull = walletFeatureToggles.isMainActionButtonsEnabled && toStatus == null
if (fromStatus == null || wallet == null || isStatusToNull) {
uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back)
} else {
userWallet = wallet
initialCryptoCurrencyStatus = cryptoCurrencyStatus
initialFromStatus = fromStatus
initialToStatus = toStatus
initTokens(isInitiallyReversed)
}
}
@ -179,7 +197,7 @@ internal class SwapViewModel @Inject constructor(
}
fun onScreenOpened() {
analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCryptoCurrency.symbol))
analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCurrencyFrom.symbol))
}
fun setRouter(router: SwapRouter) {
@ -205,14 +223,19 @@ internal class SwapViewModel @Inject constructor(
private fun initTokens(isReverseFromTo: Boolean) {
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) {
swapInteractor.getTokensDataState(initialCryptoCurrency)
swapInteractor.getTokensDataState(initialCurrencyFrom)
}.onSuccess { state ->
updateTokensState(state)
val selectedCurrency = swapInteractor.getInitialCurrencyToSwap(
initialCryptoCurrency = initialCryptoCurrency,
state = state,
isReverseFromTo = isReverseFromTo,
)
val selectedCurrency = if (walletFeatureToggles.isMainActionButtonsEnabled) {
initialToStatus
} else {
swapInteractor.getInitialCurrencyToSwap(
initialCryptoCurrency = initialCurrencyFrom,
state = state,
isReverseFromTo = isReverseFromTo,
)
}
applyInitialTokenChoice(
state = state,
selectedCurrency = selectedCurrency,
@ -248,9 +271,10 @@ internal class SwapViewModel @Inject constructor(
(it as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code,
) {
uiState = stateBuilder.createInitialLoadingState(
initialCurrency = initialCryptoCurrency,
networkInfo = blockchainInteractor.getBlockchainInfo(
initialCryptoCurrency.network.backendId,
initialCurrencyFrom = initialCurrencyFrom,
initialCurrencyTo = initialCurrencyTo,
fromNetworkInfo = blockchainInteractor.getBlockchainInfo(
initialCurrencyFrom.network.backendId,
),
)
initTokens(isReverseFromTo)
@ -269,15 +293,15 @@ internal class SwapViewModel @Inject constructor(
analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap)
uiState = stateBuilder.createNoAvailableTokensToSwapState(
uiStateHolder = uiState,
fromToken = initialCryptoCurrencyStatus,
fromToken = initialFromStatus,
)
return
}
isOrderReversed = isReverseFromTo
val (fromCurrencyStatus, toCurrencyStatus) = if (isOrderReversed) {
selectedCurrency to initialCryptoCurrencyStatus
selectedCurrency to initialFromStatus
} else {
initialCryptoCurrencyStatus to selectedCurrency
initialFromStatus to selectedCurrency
}
dataState = dataState.copy(
fromCryptoCurrency = fromCurrencyStatus,
@ -297,7 +321,7 @@ internal class SwapViewModel @Inject constructor(
uiState = stateBuilder.addTokensToState(
uiState = uiState,
tokensDataState = tokensDataState,
fromToken = dataState.fromCryptoCurrency?.currency ?: initialCryptoCurrency,
fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom,
)
}
@ -314,7 +338,7 @@ internal class SwapViewModel @Inject constructor(
uiState,
fromToken.currency,
toToken.currency,
initialCryptoCurrency.id.value,
initialCurrencyFrom.id.value,
)
}
singleTaskScheduler.scheduleTask(
@ -410,7 +434,7 @@ internal class SwapViewModel @Inject constructor(
if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) {
analyticsEventHandler.send(
SwapEvents.NoticeNotEnoughFee(
token = initialCryptoCurrency.symbol,
token = initialCurrencyFrom.symbol,
blockchain = fromToken.currency.network.name,
),
)
@ -448,7 +472,7 @@ internal class SwapViewModel @Inject constructor(
}
analyticsEventHandler.send(
SwapEvents.NoticeProviderError(
sendToken = "${initialCryptoCurrency.network.backendId}:${initialCryptoCurrency.symbol}",
sendToken = "${initialCurrencyFrom.network.backendId}:${initialCurrencyFrom.symbol}",
receiveToken = receiveToken ?: "",
provider = provider,
errorCode = error.code,
@ -586,7 +610,7 @@ internal class SwapViewModel @Inject constructor(
swapRouter.openUrl(url)
}
analyticsEventHandler.send(
event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol),
event = SwapEvents.ButtonExplore(initialCurrencyFrom.symbol),
)
},
onStatusClick = {
@ -594,7 +618,7 @@ internal class SwapViewModel @Inject constructor(
if (!txExternalUrl.isNullOrBlank()) {
swapRouter.openUrl(txExternalUrl)
analyticsEventHandler.send(
event = SwapEvents.ButtonStatus(initialCryptoCurrency.symbol),
event = SwapEvents.ButtonStatus(initialCurrencyFrom.symbol),
)
}
},
@ -765,7 +789,7 @@ internal class SwapViewModel @Inject constructor(
val toToken: CryptoCurrencyStatus
if (isOrderReversed) {
fromToken = foundToken.currencyStatus
toToken = initialCryptoCurrencyStatus
toToken = initialFromStatus
val newToken = fromToken.currency as? CryptoCurrency.Coin
if (newToken != null) {
@ -776,7 +800,7 @@ internal class SwapViewModel @Inject constructor(
)
}
} else {
fromToken = initialCryptoCurrencyStatus
fromToken = initialFromStatus
toToken = foundToken.currencyStatus
val newToken = toToken.currency as? CryptoCurrency.Coin
@ -1264,7 +1288,7 @@ internal class SwapViewModel @Inject constructor(
private fun onFailedTxEmailClick(errorMessage: String) {
viewModelScope.launch {
val network = initialCryptoCurrencyStatus.currency.network
val network = initialFromStatus.currency.network
val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") }
saveBlockchainErrorUseCase(
@ -1273,7 +1297,7 @@ internal class SwapViewModel @Inject constructor(
blockchainId = network.id.value,
derivationPath = network.derivationPath.value,
destinationAddress = dataState.swapDataModel?.transaction?.txTo.orEmpty(),
tokenSymbol = initialCryptoCurrency.symbol,
tokenSymbol = initialCurrencyFrom.symbol,
amount = dataState.amount.orEmpty(),
fee = dataState.selectedFee?.feeCryptoFormatted.orEmpty(),
),

View file

@ -621,7 +621,7 @@ internal class TokenDetailsViewModel @Inject constructor(
if (handleUnavailabilityReason(unavailabilityReason)) return
appRouter.push(AppRoute.Swap(currency = cryptoCurrency, userWalletId = userWalletId))
appRouter.push(AppRoute.Swap(currencyFrom = cryptoCurrency, userWalletId = userWalletId))
}
override fun onDismissDialog() {

View file

@ -380,7 +380,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
appRouter.push(
AppRoute.Swap(
currency = cryptoCurrencyStatus.currency,
currencyFrom = cryptoCurrencyStatus.currency,
userWalletId = userWalletId,
),
)