Updated on 2026-08-14
This commit is contained in:
parent
9c296c7ced
commit
a9dbcbc0d1
14 changed files with 687 additions and 90 deletions
|
|
@ -36,6 +36,8 @@ dependencies {
|
|||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.express.models)
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.account.status)
|
||||
|
||||
implementation(projects.features.swap.domain.api)
|
||||
implementation(projects.features.swap.domain.models)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.swap.domain.models.ui
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
|
|
@ -79,6 +80,7 @@ data class TokenSwapInfo(
|
|||
val tokenAmount: SwapAmount,
|
||||
val amountFiat: BigDecimal,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val account: Account.CryptoPortfolio?,
|
||||
)
|
||||
|
||||
data class RequestApproveStateData(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.feature.swap.domain.models.ui
|
||||
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
|
||||
|
|
@ -9,12 +11,11 @@ data class TokensDataStateExpress(
|
|||
val allProviders: List<SwapProvider>,
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY =
|
||||
TokensDataStateExpress(
|
||||
fromGroup = CurrenciesGroup(emptyList(), emptyList(), false),
|
||||
toGroup = CurrenciesGroup(emptyList(), emptyList(), false),
|
||||
allProviders = emptyList(),
|
||||
)
|
||||
val EMPTY = TokensDataStateExpress(
|
||||
fromGroup = CurrenciesGroup(emptyList(), emptyList(), emptyList(), false),
|
||||
toGroup = CurrenciesGroup(emptyList(), emptyList(), emptyList(), false),
|
||||
allProviders = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,5 +30,18 @@ fun TokensDataStateExpress.getGroupWithReverse(isReverseFromTo: Boolean): Curren
|
|||
data class CurrenciesGroup(
|
||||
val available: List<CryptoCurrencySwapInfo>,
|
||||
val unavailable: List<CryptoCurrencySwapInfo>,
|
||||
val accountCurrencyList: List<AccountSwapAvailability>,
|
||||
val isAfterSearch: Boolean,
|
||||
)
|
||||
|
||||
data class AccountSwapAvailability(
|
||||
val account: Account.CryptoPortfolio,
|
||||
val currencyList: List<AccountSwapCurrency>,
|
||||
)
|
||||
|
||||
data class AccountSwapCurrency(
|
||||
val isAvailable: Boolean,
|
||||
val account: Account.CryptoPortfolio,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val providers: List<SwapProvider>,
|
||||
)
|
||||
|
|
@ -3,8 +3,10 @@ package com.tangem.feature.swap.domain
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.feature.swap.domain.models.ui.AccountSwapCurrency
|
||||
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
|
||||
import com.tangem.feature.swap.domain.models.ui.getGroupWithReverse
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultInitialToCurrencyResolver(
|
||||
|
|
@ -27,10 +29,40 @@ internal class DefaultInitialToCurrencyResolver(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun tryGetFromCacheV2(
|
||||
userWallet: UserWallet,
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
isReverseFromTo: Boolean,
|
||||
): AccountSwapCurrency? {
|
||||
val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(userWallet.walletId) ?: return null
|
||||
|
||||
return if (id != initialCryptoCurrency.id.value) {
|
||||
val group = state.getGroupWithReverse(isReverseFromTo)
|
||||
|
||||
group.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) ->
|
||||
currencyList.find { it.isAvailable && it.cryptoCurrencyStatus.currency.id.value == id }
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): CryptoCurrencyStatus? {
|
||||
val group = state.getGroupWithReverse(isReverseFromTo)
|
||||
return group.available.maxByOrNull {
|
||||
it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO
|
||||
}?.currencyStatus
|
||||
}
|
||||
|
||||
override fun tryGetWithMaxAmountV2(state: TokensDataStateExpress, isReverseFromTo: Boolean): AccountSwapCurrency? {
|
||||
val group = state.getGroupWithReverse(isReverseFromTo)
|
||||
return group.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) ->
|
||||
currencyList.maxByOrNull { swapAccountCurrency ->
|
||||
swapAccountCurrency.cryptoCurrencyStatus.value.fiatAmount
|
||||
.takeIf { swapAccountCurrency.isAvailable }
|
||||
.orZero()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.feature.swap.domain
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.feature.swap.domain.models.ui.AccountSwapCurrency
|
||||
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
|
||||
|
||||
interface InitialToCurrencyResolver {
|
||||
|
|
@ -15,4 +16,13 @@ interface InitialToCurrencyResolver {
|
|||
): CryptoCurrencyStatus?
|
||||
|
||||
fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): CryptoCurrencyStatus?
|
||||
|
||||
suspend fun tryGetFromCacheV2(
|
||||
userWallet: UserWallet,
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
isReverseFromTo: Boolean,
|
||||
): AccountSwapCurrency?
|
||||
|
||||
fun tryGetWithMaxAmountV2(state: TokensDataStateExpress, isReverseFromTo: Boolean): AccountSwapCurrency?
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.domain.express.models.ExpressOperationType
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -29,16 +30,23 @@ interface SwapInteractor {
|
|||
* Find best quote for given tokens to swap
|
||||
* under the hood calls different methods to receive data, depends on permission for given token
|
||||
*
|
||||
* @param fromToken [Currency] from which want to swap
|
||||
* @param toToken [Currency] that receive after swap
|
||||
* @param amountToSwap amount you want to swap
|
||||
* @param selectedFee selected fee to swap
|
||||
* @param fromToken token from which want to swap
|
||||
* @param fromAccount account from which swap will be made
|
||||
* @param toToken token that receive after swap
|
||||
* @param toAccount account to which receive token after swap
|
||||
* @param providers list of providers to find quote
|
||||
* @param amountToSwap amount you want to swap
|
||||
* @param reduceBalanceBy amount to reduce from balance (used for fee calculation)
|
||||
* @param selectedFee selected fee to swap
|
||||
* @return
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun findBestQuote(
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
providers: List<SwapProvider>,
|
||||
amountToSwap: String,
|
||||
reduceBalanceBy: BigDecimal,
|
||||
|
|
@ -90,6 +98,19 @@ interface SwapInteractor {
|
|||
isReverseFromTo: Boolean,
|
||||
): CryptoCurrencyStatus?
|
||||
|
||||
/**
|
||||
* Returns initial currency to swap as AccountSwapCurrency
|
||||
*
|
||||
* @param initialCryptoCurrency initial currency selected to swap
|
||||
* @param state current tokens data state
|
||||
* @param isReverseFromTo flag indicating the direction of the swap
|
||||
*/
|
||||
suspend fun getInitialCurrencyToSwapV2(
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
isReverseFromTo: Boolean,
|
||||
): AccountSwapCurrency?
|
||||
|
||||
fun getNativeToken(networkId: String): CryptoCurrency
|
||||
|
||||
interface Factory {
|
||||
|
|
|
|||
|
|
@ -16,12 +16,16 @@ import com.tangem.blockchainsdk.utils.toBlockchain
|
|||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.extenstions.unwrap
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.express.models.ExpressOperationType
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -87,6 +91,8 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase,
|
||||
private val amountFormatter: AmountFormatter,
|
||||
private val rampStateManager: RampStateManager,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
@Assisted private val userWalletId: UserWalletId,
|
||||
) : SwapInteractor {
|
||||
|
||||
|
|
@ -102,7 +108,16 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress {
|
||||
val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWalletId)
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
getAccountCurrencyTokensDataState(currency)
|
||||
} else {
|
||||
getCurrencyTokensDataState(currency)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress {
|
||||
val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase
|
||||
.invokeMultiWalletSync(userWalletId)
|
||||
.getOrElse { emptyList() }
|
||||
|
||||
val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses
|
||||
|
|
@ -146,6 +161,65 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun getAccountCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress {
|
||||
val walletAccountCurrencyStatuses = singleAccountStatusListSupplier.getSyncOrNull(
|
||||
SingleAccountStatusListProducer.Params(userWalletId),
|
||||
)?.accountStatuses.orEmpty()
|
||||
|
||||
val walletAccountCurrencyStatusesExceptInitial = walletAccountCurrencyStatuses
|
||||
.mapNotNull { accountStatus ->
|
||||
val filteredCurrencies = accountStatus.flattenCurrencies().filter {
|
||||
val currencyFilter = it.currency.network.backendId != currency.network.backendId ||
|
||||
it.currency.getContractAddress() != currency.getContractAddress()
|
||||
|
||||
val statusFilter =
|
||||
it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount
|
||||
val notCustomTokenFilter = !it.currency.isCustom
|
||||
|
||||
statusFilter && currencyFilter && notCustomTokenFilter
|
||||
}
|
||||
|
||||
if (filteredCurrencies.isNotEmpty()) {
|
||||
accountStatus.account to filteredCurrencies
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
if (walletAccountCurrencyStatusesExceptInitial.isEmpty()) {
|
||||
return TokensDataStateExpress.EMPTY
|
||||
}
|
||||
|
||||
val pairsLeast = getPairs(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = LeastTokenInfo(
|
||||
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0",
|
||||
network = currency.network.backendId,
|
||||
),
|
||||
currenciesList = walletAccountCurrencyStatusesExceptInitial.flatMap { accountStatus ->
|
||||
accountStatus.value.map { it.currency }
|
||||
},
|
||||
)
|
||||
|
||||
return TokensDataStateExpress(
|
||||
fromGroup = getToCurrenciesGroupV2(
|
||||
currency = currency,
|
||||
leastPairs = pairsLeast.pairs,
|
||||
cryptoCurrenciesList = walletAccountCurrencyStatusesExceptInitial,
|
||||
tokenInfoForFilter = { it.to },
|
||||
tokenInfoForAvailable = { it.from },
|
||||
),
|
||||
toGroup = getToCurrenciesGroupV2(
|
||||
currency = currency,
|
||||
leastPairs = pairsLeast.pairs,
|
||||
cryptoCurrenciesList = walletAccountCurrencyStatusesExceptInitial,
|
||||
tokenInfoForFilter = { it.from },
|
||||
tokenInfoForAvailable = { it.to },
|
||||
),
|
||||
allProviders = pairsLeast.allProviders,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getToCurrenciesGroup(
|
||||
currency: CryptoCurrency,
|
||||
leastPairs: List<SwapPairLeast>,
|
||||
|
|
@ -174,6 +248,49 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
return CurrenciesGroup(
|
||||
available = availableCryptoCurrencies,
|
||||
unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) },
|
||||
accountCurrencyList = emptyList(),
|
||||
isAfterSearch = false,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getToCurrenciesGroupV2(
|
||||
currency: CryptoCurrency,
|
||||
leastPairs: List<SwapPairLeast>,
|
||||
cryptoCurrenciesList: Map<Account, List<CryptoCurrencyStatus>>,
|
||||
tokenInfoForFilter: (SwapPairLeast) -> LeastTokenInfo,
|
||||
tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo,
|
||||
): CurrenciesGroup {
|
||||
val filteredPairs = leastPairs.filter {
|
||||
tokenInfoForFilter(it).contractAddress == currency.getContractAddress() &&
|
||||
tokenInfoForFilter(it).network == currency.network.backendId
|
||||
}
|
||||
|
||||
val accountCurrencyList = cryptoCurrenciesList.mapNotNull { (account, currencyStatusList) ->
|
||||
val account = account as? Account.CryptoPortfolio ?: return@mapNotNull null
|
||||
|
||||
AccountSwapAvailability(
|
||||
account = account,
|
||||
currencyList = currencyStatusList.map { currencyStatus ->
|
||||
val providers = findProvidersForPair(
|
||||
cryptoCurrencyStatuses = currencyStatus,
|
||||
swapPairsLeastList = filteredPairs,
|
||||
tokenInfoForAvailable = tokenInfoForAvailable,
|
||||
)
|
||||
val isUnavailable = providers.isNullOrEmpty()
|
||||
AccountSwapCurrency(
|
||||
isAvailable = !isUnavailable,
|
||||
account = account,
|
||||
cryptoCurrencyStatus = currencyStatus,
|
||||
providers = providers.orEmpty(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return CurrenciesGroup(
|
||||
available = emptyList(),
|
||||
unavailable = emptyList(),
|
||||
accountCurrencyList = accountCurrencyList,
|
||||
isAfterSearch = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -257,7 +374,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
|
||||
override suspend fun findBestQuote(
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
providers: List<SwapProvider>,
|
||||
amountToSwap: String,
|
||||
reduceBalanceBy: BigDecimal,
|
||||
|
|
@ -288,7 +407,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
manageDexSolana(
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
provider = provider,
|
||||
selectedFee = selectedFee,
|
||||
amount = amount,
|
||||
|
|
@ -299,7 +420,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
manageDex(
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
provider = provider,
|
||||
selectedFee = selectedFee,
|
||||
amount = amount,
|
||||
|
|
@ -312,7 +435,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
manageCex(
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
provider = provider,
|
||||
amount = amount,
|
||||
reduceBalanceBy = reduceBalanceBy,
|
||||
|
|
@ -327,7 +452,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
private suspend fun manageDex(
|
||||
networkId: String,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
provider: SwapProvider,
|
||||
selectedFee: FeeType,
|
||||
amount: SwapAmount,
|
||||
|
|
@ -338,6 +465,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
return provider to produceDexSwapDataError(
|
||||
error = ExpressDataError.DexActiveSupplyError,
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
amount = amount,
|
||||
)
|
||||
}
|
||||
|
|
@ -374,7 +502,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
provider = provider,
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
amount = amount,
|
||||
selectedFee = selectedFee,
|
||||
expressOperationType = expressOperationType,
|
||||
|
|
@ -385,7 +515,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
quoteDataModel = maybeQuotes,
|
||||
amount = amount,
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
networkId = networkId,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
|
||||
|
|
@ -400,7 +532,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
private suspend fun manageDexSolana(
|
||||
networkId: String,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
provider: SwapProvider,
|
||||
selectedFee: FeeType,
|
||||
amount: SwapAmount,
|
||||
|
|
@ -425,7 +559,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
provider = provider,
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
amount = amount,
|
||||
selectedFee = selectedFee,
|
||||
expressOperationType = expressOperationType,
|
||||
|
|
@ -436,7 +572,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
quoteDataModel = maybeQuotes,
|
||||
amount = amount,
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
networkId = networkId,
|
||||
isAllowedToSpend = true,
|
||||
isBalanceWithoutFeeEnough = false,
|
||||
|
|
@ -451,7 +589,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
private suspend fun manageCex(
|
||||
networkId: String,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
provider: SwapProvider,
|
||||
amount: SwapAmount,
|
||||
reduceBalanceBy: BigDecimal,
|
||||
|
|
@ -463,7 +603,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
amount = amount,
|
||||
reduceBalanceBy = reduceBalanceBy,
|
||||
fromTokenStatus = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toTokenStatus = toToken,
|
||||
toAccount = toAccount,
|
||||
isAllowedToSpend = true,
|
||||
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
|
||||
provider = provider,
|
||||
|
|
@ -976,6 +1118,17 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
?: group.available.firstOrNull()?.currencyStatus
|
||||
}
|
||||
|
||||
override suspend fun getInitialCurrencyToSwapV2(
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
isReverseFromTo: Boolean,
|
||||
): AccountSwapCurrency? {
|
||||
val group = state.getGroupWithReverse(isReverseFromTo)
|
||||
return initialToCurrencyResolver.tryGetFromCacheV2(userWallet, initialCryptoCurrency, state, isReverseFromTo)
|
||||
?: initialToCurrencyResolver.tryGetWithMaxAmountV2(state, isReverseFromTo)
|
||||
?: group.accountCurrencyList.firstNotNullOfOrNull { it.currencyList.firstOrNull { it.isAvailable } }
|
||||
}
|
||||
|
||||
override fun getNativeToken(networkId: String): CryptoCurrency {
|
||||
return repository.getNativeTokenForNetwork(networkId)
|
||||
}
|
||||
|
|
@ -1020,7 +1173,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
amount: SwapAmount,
|
||||
reduceBalanceBy: BigDecimal,
|
||||
fromTokenStatus: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toTokenStatus: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
provider: SwapProvider,
|
||||
isAllowedToSpend: Boolean,
|
||||
isBalanceWithoutFeeEnough: Boolean,
|
||||
|
|
@ -1073,7 +1228,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
quoteDataModel = quotes,
|
||||
amount = amount,
|
||||
fromToken = fromTokenStatus,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toTokenStatus,
|
||||
toAccount = toAccount,
|
||||
networkId = networkId,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
|
||||
|
|
@ -1091,7 +1248,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
quoteDataModel: Either<ExpressDataError, QuoteModel>,
|
||||
amount: SwapAmount,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
networkId: String,
|
||||
isAllowedToSpend: Boolean,
|
||||
isBalanceWithoutFeeEnough: Boolean,
|
||||
|
|
@ -1105,7 +1264,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
val swapState = updateBalances(
|
||||
networkId = networkId,
|
||||
fromTokenStatus = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toTokenStatus = toToken,
|
||||
toAccount = toAccount,
|
||||
fromTokenAmount = amount,
|
||||
toTokenAmount = quoteModel.toTokenAmount,
|
||||
swapData = null,
|
||||
|
|
@ -1133,6 +1294,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
val state = updatePermissionState(
|
||||
networkId = networkId,
|
||||
fromTokenStatus = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
swapAmount = amount,
|
||||
quotesLoadedState = swapState,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
|
|
@ -1174,6 +1336,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
ifLeft = { error ->
|
||||
createSwapErrorWith(
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
amount = amount,
|
||||
includeFeeInAmount = includeFeeInAmount,
|
||||
expressDataError = error,
|
||||
|
|
@ -1184,6 +1347,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
|
||||
private suspend fun createSwapErrorWith(
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
amount: SwapAmount,
|
||||
includeFeeInAmount: IncludeFeeInAmount,
|
||||
expressDataError: ExpressDataError,
|
||||
|
|
@ -1194,6 +1358,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
amountFiat = rates[fromToken.currency.id]?.multiply(amount.value)
|
||||
?: BigDecimal.ZERO,
|
||||
cryptoCurrencyStatus = fromToken,
|
||||
account = fromAccount,
|
||||
)
|
||||
return SwapState.SwapError(fromTokenSwapInfo, expressDataError, includeFeeInAmount)
|
||||
}
|
||||
|
|
@ -1307,7 +1472,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
provider: SwapProvider,
|
||||
networkId: String,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
amount: SwapAmount,
|
||||
selectedFee: FeeType,
|
||||
expressOperationType: ExpressOperationType,
|
||||
|
|
@ -1345,6 +1512,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
return produceDexSwapDataError(
|
||||
error = ExpressDataError.TooLargeSolanaTransactionError,
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
amount = amount,
|
||||
)
|
||||
}
|
||||
|
|
@ -1384,7 +1552,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
val swapState = updateBalances(
|
||||
networkId = networkId,
|
||||
fromTokenStatus = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toTokenStatus = toToken,
|
||||
toAccount = toAccount,
|
||||
fromTokenAmount = amount,
|
||||
toTokenAmount = swapData.toTokenAmount,
|
||||
swapData = swapData,
|
||||
|
|
@ -1413,6 +1583,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
produceDexSwapDataError(
|
||||
error = error,
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
amount = amount,
|
||||
)
|
||||
},
|
||||
|
|
@ -1422,6 +1593,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
private suspend fun produceDexSwapDataError(
|
||||
error: ExpressDataError,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
amount: SwapAmount,
|
||||
): SwapState.SwapError {
|
||||
val rates = getQuotes(fromToken.currency.id)
|
||||
|
|
@ -1430,6 +1602,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
amountFiat = rates[fromToken.currency.id]?.multiply(amount.value)
|
||||
?: BigDecimal.ZERO,
|
||||
cryptoCurrencyStatus = fromToken,
|
||||
account = fromAccount,
|
||||
)
|
||||
return SwapState.SwapError(
|
||||
fromTokenSwapInfo,
|
||||
|
|
@ -1498,7 +1671,9 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
provider: SwapProvider,
|
||||
networkId: String,
|
||||
fromTokenStatus: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toTokenStatus: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
fromTokenAmount: SwapAmount,
|
||||
toTokenAmount: SwapAmount,
|
||||
swapData: SwapDataModel?,
|
||||
|
|
@ -1511,6 +1686,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
return SwapState.QuotesLoadedState(
|
||||
fromTokenInfo = TokenSwapInfo(
|
||||
tokenAmount = fromTokenAmount,
|
||||
account = fromAccount,
|
||||
cryptoCurrencyStatus = fromTokenStatus,
|
||||
amountFiat = rates[fromToken.id]?.multiply(fromTokenAmount.value)
|
||||
?: BigDecimal.ZERO,
|
||||
|
|
@ -1518,6 +1694,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
toTokenInfo = TokenSwapInfo(
|
||||
tokenAmount = toTokenAmount,
|
||||
cryptoCurrencyStatus = toTokenStatus,
|
||||
account = toAccount,
|
||||
amountFiat = rates[toToken.id]?.multiply(toTokenAmount.value)
|
||||
?: BigDecimal.ZERO,
|
||||
),
|
||||
|
|
@ -1564,6 +1741,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
private suspend fun updatePermissionState(
|
||||
networkId: String,
|
||||
fromTokenStatus: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
swapAmount: SwapAmount,
|
||||
quotesLoadedState: SwapState.QuotesLoadedState,
|
||||
spenderAddress: String?,
|
||||
|
|
@ -1620,6 +1798,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
// it's impossible next steps without fee
|
||||
return createSwapErrorWith(
|
||||
fromToken = fromTokenStatus,
|
||||
fromAccount = fromAccount,
|
||||
amount = swapAmount,
|
||||
includeFeeInAmount = IncludeFeeInAmount.Excluded,
|
||||
expressDataError = ExpressDataError.UnknownError,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ dependencies {
|
|||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.express.models)
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.account.status)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.swap.domain)
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@ import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo
|
|||
import com.tangem.feature.swap.models.CurrenciesGroupWithFromCurrency
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.TokenBalanceData
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import com.tangem.feature.swap.models.TokenToSelectState
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class TokensDataConverter(
|
||||
internal class TokensDataConverter(
|
||||
private val onSearchEntered: (String) -> Unit,
|
||||
private val onTokenSelected: (String) -> Unit,
|
||||
private val isBalanceHiddenProvider: Provider<Boolean>,
|
||||
|
|
@ -53,6 +54,7 @@ class TokensDataConverter(
|
|||
}
|
||||
}
|
||||
.toImmutableList(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
onSearchEntered = onSearchEntered,
|
||||
onTokenSelected = onTokenSelected,
|
||||
afterSearch = group.isAfterSearch,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ import com.tangem.core.navigation.url.UrlOpener
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.InputNumberFormatter
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
|
|
@ -29,6 +33,7 @@ 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.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -105,6 +110,9 @@ internal class SwapModel @Inject constructor(
|
|||
swapInteractorFactory: SwapInteractor.Factory,
|
||||
private val urlOpener: UrlOpener,
|
||||
router: AppRouter,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<SwapComponent.Params>()
|
||||
|
|
@ -158,6 +166,10 @@ internal class SwapModel @Inject constructor(
|
|||
private var swapRouter: SwapRouter = SwapRouter(router = router)
|
||||
private var userCountry: UserCountry? = null
|
||||
|
||||
private lateinit var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus
|
||||
private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null
|
||||
private var isAccountsMode: Boolean = false
|
||||
|
||||
private val isUserResolvableError: (SwapState) -> Boolean = {
|
||||
it is SwapState.SwapError &&
|
||||
(
|
||||
|
|
@ -183,19 +195,48 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
modelScope.launch(dispatchers.io) {
|
||||
val fromStatus =
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWalletId, initialCurrencyFrom.id)
|
||||
.getOrNull()
|
||||
val toStatus = initialCurrencyTo?.let {
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWalletId, it.id).getOrNull()
|
||||
}
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
isAccountsMode = isAccountsModeEnabledUseCase.invokeSync()
|
||||
|
||||
if (fromStatus == null) {
|
||||
uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back)
|
||||
val fromAccountStatus = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = userWalletId,
|
||||
currency = initialCurrencyFrom,
|
||||
).getOrNull()
|
||||
val toAccountStatus = initialCurrencyTo?.let {
|
||||
getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = userWalletId,
|
||||
currency = it,
|
||||
).getOrNull()
|
||||
}
|
||||
|
||||
if (fromAccountStatus == null) {
|
||||
uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back)
|
||||
} else {
|
||||
fromAccountCurrencyStatus = fromAccountStatus
|
||||
toAccountCurrencyStatus = toAccountStatus
|
||||
initialFromStatus = fromAccountStatus.status
|
||||
initialToStatus = toAccountStatus?.status
|
||||
initTokens(isInitiallyReversed)
|
||||
}
|
||||
} else {
|
||||
initialFromStatus = fromStatus
|
||||
initialToStatus = toStatus
|
||||
initTokens(isInitiallyReversed)
|
||||
val fromStatus = getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyId = initialCurrencyFrom.id,
|
||||
).getOrNull()
|
||||
val toStatus = initialCurrencyTo?.let {
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyId = it.id,
|
||||
).getOrNull()
|
||||
}
|
||||
|
||||
if (fromStatus == null) {
|
||||
uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back)
|
||||
} else {
|
||||
initialFromStatus = fromStatus
|
||||
initialToStatus = toStatus
|
||||
initTokens(isInitiallyReversed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +266,10 @@ internal class SwapModel @Inject constructor(
|
|||
private fun sendSelectTokenScreenOpenedEvent() {
|
||||
val isAnyAvailableTokensTo = dataState.tokensDataState?.toGroup?.available?.isNotEmpty() == true
|
||||
val isAnyAvailableTokensFrom = dataState.tokensDataState?.fromGroup?.available?.isNotEmpty() == true
|
||||
val isAnyAvailableTokens = isAnyAvailableTokensTo || isAnyAvailableTokensFrom
|
||||
val isAnyAvailableAccountTokensTo = !dataState.tokensDataState?.toGroup?.accountCurrencyList.isNullOrEmpty()
|
||||
val isAnyAvailableAccountTokensFrom = !dataState.tokensDataState?.fromGroup?.accountCurrencyList.isNullOrEmpty()
|
||||
val isAnyAvailableTokens = isAnyAvailableTokensTo || isAnyAvailableTokensFrom ||
|
||||
isAnyAvailableAccountTokensTo || isAnyAvailableAccountTokensFrom
|
||||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(availableTokens = isAnyAvailableTokens))
|
||||
}
|
||||
|
||||
|
|
@ -235,15 +279,32 @@ internal class SwapModel @Inject constructor(
|
|||
swapInteractor.getTokensDataState(initialCurrencyFrom)
|
||||
}.onSuccess { state ->
|
||||
updateTokensState(state)
|
||||
val selectedCurrency = initialToStatus ?: swapInteractor.getInitialCurrencyToSwap(
|
||||
initialCryptoCurrency = initialCurrencyFrom,
|
||||
state = state,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
|
||||
val (selectedCurrency, selectedAccount) = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
val selectedAccountCurrency = toAccountCurrencyStatus ?: swapInteractor.getInitialCurrencyToSwapV2(
|
||||
initialCryptoCurrency = initialCurrencyFrom,
|
||||
state = state,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)?.let {
|
||||
AccountCryptoCurrencyStatus(
|
||||
account = it.account,
|
||||
status = it.cryptoCurrencyStatus,
|
||||
)
|
||||
}
|
||||
selectedAccountCurrency?.status to selectedAccountCurrency?.account
|
||||
} else {
|
||||
val selectedCurrency = initialToStatus ?: swapInteractor.getInitialCurrencyToSwap(
|
||||
initialCryptoCurrency = initialCurrencyFrom,
|
||||
state = state,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
selectedCurrency to null
|
||||
}
|
||||
|
||||
applyInitialTokenChoice(
|
||||
state = state,
|
||||
selectedCurrency = selectedCurrency,
|
||||
selectedAccount = selectedAccount,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
|
||||
|
|
@ -268,6 +329,7 @@ internal class SwapModel @Inject constructor(
|
|||
applyInitialTokenChoice(
|
||||
state = TokensDataStateExpress.EMPTY,
|
||||
selectedCurrency = null,
|
||||
selectedAccount = null,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
|
||||
|
|
@ -304,6 +366,7 @@ internal class SwapModel @Inject constructor(
|
|||
private fun applyInitialTokenChoice(
|
||||
state: TokensDataStateExpress,
|
||||
selectedCurrency: CryptoCurrencyStatus?,
|
||||
selectedAccount: Account.CryptoPortfolio?,
|
||||
isReverseFromTo: Boolean,
|
||||
) {
|
||||
// exceptional case
|
||||
|
|
@ -321,14 +384,27 @@ internal class SwapModel @Inject constructor(
|
|||
} else {
|
||||
initialFromStatus to selectedCurrency
|
||||
}
|
||||
val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
if (isOrderReversed) {
|
||||
selectedAccount to fromAccountCurrencyStatus.account
|
||||
} else {
|
||||
fromAccountCurrencyStatus.account to selectedAccount
|
||||
}
|
||||
} else {
|
||||
null to null
|
||||
}
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = fromCurrencyStatus,
|
||||
fromAccount = fromAccount,
|
||||
toCryptoCurrency = toCurrencyStatus,
|
||||
toAccount = toAccount,
|
||||
tokensDataState = state,
|
||||
)
|
||||
startLoadingQuotes(
|
||||
fromToken = fromCurrencyStatus,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toCurrencyStatus,
|
||||
toAccount = toAccount,
|
||||
amount = lastAmount.value,
|
||||
reduceBalanceBy = lastReducedBalanceBy.value,
|
||||
toProvidersList = findSwapProviders(fromCurrencyStatus, toCurrencyStatus),
|
||||
|
|
@ -337,16 +413,28 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private fun updateTokensState(tokenDataState: TokensDataStateExpress) {
|
||||
val tokensDataState = if (isOrderReversed) tokenDataState.fromGroup else tokenDataState.toGroup
|
||||
uiState = stateBuilder.addTokensToState(
|
||||
uiState = uiState,
|
||||
tokensDataState = tokensDataState,
|
||||
fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom,
|
||||
)
|
||||
|
||||
uiState = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
// stateBuilder.addTokensToStateV2(
|
||||
// uiState = uiState,
|
||||
// tokensDataState = tokensDataState,
|
||||
// isAccountsMode = isAccountsMode,
|
||||
// )
|
||||
TODO()
|
||||
} else {
|
||||
stateBuilder.addTokensToState(
|
||||
uiState = uiState,
|
||||
tokensDataState = tokensDataState,
|
||||
fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLoadingQuotes(
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
amount: String,
|
||||
reduceBalanceBy: BigDecimal,
|
||||
toProvidersList: List<SwapProvider>,
|
||||
|
|
@ -355,17 +443,19 @@ internal class SwapModel @Inject constructor(
|
|||
singleTaskScheduler.cancelTask()
|
||||
if (!isSilent) {
|
||||
uiState = stateBuilder.createQuotesLoadingState(
|
||||
uiState,
|
||||
fromToken.currency,
|
||||
toToken.currency,
|
||||
initialCurrencyFrom.id.value,
|
||||
uiStateHolder = uiState,
|
||||
fromToken = fromToken.currency,
|
||||
toToken = toToken.currency,
|
||||
mainTokenId = initialCurrencyFrom.id.value,
|
||||
)
|
||||
}
|
||||
singleTaskScheduler.scheduleTask(
|
||||
modelScope,
|
||||
loadQuotesTask(
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
amount = amount,
|
||||
reduceBalanceBy = reduceBalanceBy,
|
||||
toProvidersList = toProvidersList,
|
||||
|
|
@ -380,7 +470,9 @@ internal class SwapModel @Inject constructor(
|
|||
if (fromCurrency != null && toCurrency != null && amount != null) {
|
||||
startLoadingQuotes(
|
||||
fromToken = fromCurrency,
|
||||
fromAccount = dataState.fromAccount,
|
||||
toToken = toCurrency,
|
||||
toAccount = dataState.toAccount,
|
||||
amount = amount,
|
||||
isSilent = isSilent,
|
||||
reduceBalanceBy = dataState.reduceBalanceBy,
|
||||
|
|
@ -391,7 +483,9 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private fun loadQuotesTask(
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
amount: String,
|
||||
reduceBalanceBy: BigDecimal,
|
||||
toProvidersList: List<SwapProvider>,
|
||||
|
|
@ -409,7 +503,9 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
swapInteractor.findBestQuote(
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
providers = toProvidersList,
|
||||
amountToSwap = amount,
|
||||
reduceBalanceBy = reduceBalanceBy,
|
||||
|
|
@ -774,6 +870,7 @@ internal class SwapModel @Inject constructor(
|
|||
} else {
|
||||
tokenDataState.toGroup
|
||||
}
|
||||
|
||||
val available = group.available.filter {
|
||||
it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
|
|
@ -782,11 +879,28 @@ internal class SwapModel @Inject constructor(
|
|||
it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
val accountCurrencyList = group.accountCurrencyList.mapNotNull { accountSwapAvailability ->
|
||||
val filteredCurrencies = accountSwapAvailability.currencyList.filter { accountSwapCurrency ->
|
||||
val currency = accountSwapCurrency.cryptoCurrencyStatus.currency
|
||||
currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
|
||||
if (filteredCurrencies.isEmpty()) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
accountSwapAvailability.copy(
|
||||
currencyList = filteredCurrencies,
|
||||
)
|
||||
}
|
||||
|
||||
val filteredTokenDataState = if (isOrderReversed) {
|
||||
tokenDataState.copy(
|
||||
fromGroup = tokenDataState.fromGroup.copy(
|
||||
available = available,
|
||||
unavailable = unavailable,
|
||||
accountCurrencyList = accountCurrencyList,
|
||||
isAfterSearch = true,
|
||||
),
|
||||
)
|
||||
|
|
@ -795,6 +909,7 @@ internal class SwapModel @Inject constructor(
|
|||
toGroup = tokenDataState.toGroup.copy(
|
||||
available = available,
|
||||
unavailable = unavailable,
|
||||
accountCurrencyList = accountCurrencyList,
|
||||
isAfterSearch = true,
|
||||
),
|
||||
)
|
||||
|
|
@ -805,25 +920,26 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private fun onTokenSelect(id: String) {
|
||||
val tokens = dataState.tokensDataState ?: return
|
||||
val foundToken = if (isOrderReversed) {
|
||||
tokens.fromGroup.available.firstOrNull {
|
||||
it.currencyStatus.currency.id.value == id
|
||||
}
|
||||
} else {
|
||||
tokens.toGroup.available.firstOrNull {
|
||||
it.currencyStatus.currency.id.value == id
|
||||
}
|
||||
}
|
||||
foundToken?.currencyStatus?.currency?.symbol?.let {
|
||||
val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id)
|
||||
|
||||
foundToken?.currency?.symbol?.let {
|
||||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = true, token = it))
|
||||
}
|
||||
|
||||
if (foundToken != null) {
|
||||
val fromToken: CryptoCurrencyStatus
|
||||
val fromAccount: Account.CryptoPortfolio?
|
||||
val toToken: CryptoCurrencyStatus
|
||||
val toAccount: Account.CryptoPortfolio?
|
||||
if (isOrderReversed) {
|
||||
fromToken = foundToken.currencyStatus
|
||||
fromToken = foundToken
|
||||
fromAccount = foundAccount
|
||||
toToken = initialFromStatus
|
||||
toAccount = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
fromAccountCurrencyStatus.account
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val newToken = fromToken.currency as? CryptoCurrency.Coin
|
||||
if (newToken != null) {
|
||||
|
|
@ -835,7 +951,13 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
} else {
|
||||
fromToken = initialFromStatus
|
||||
toToken = foundToken.currencyStatus
|
||||
fromAccount = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
fromAccountCurrencyStatus.account
|
||||
} else {
|
||||
null
|
||||
}
|
||||
toToken = foundToken
|
||||
toAccount = foundAccount
|
||||
|
||||
val newToken = toToken.currency as? CryptoCurrency.Coin
|
||||
if (newToken != null) {
|
||||
|
|
@ -853,12 +975,16 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toCryptoCurrency = toToken,
|
||||
toAccount = toAccount,
|
||||
selectedProvider = null,
|
||||
)
|
||||
startLoadingQuotes(
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
amount = lastAmount.value,
|
||||
reduceBalanceBy = lastReducedBalanceBy.value,
|
||||
toProvidersList = findSwapProviders(fromToken, toToken),
|
||||
|
|
@ -868,6 +994,28 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getSelectedTokenAndAccount(
|
||||
tokens: TokensDataStateExpress,
|
||||
id: String,
|
||||
): Pair<CryptoCurrencyStatus?, Account.CryptoPortfolio?> {
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
val accountCryptoCurrencyStatus = if (isOrderReversed) {
|
||||
tokens.fromGroup
|
||||
} else {
|
||||
tokens.toGroup
|
||||
}.accountCurrencyList.firstNotNullOfOrNull {
|
||||
it.currencyList.firstOrNull { it.cryptoCurrencyStatus.currency.id.value == id }
|
||||
}
|
||||
accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account
|
||||
} else {
|
||||
if (isOrderReversed) {
|
||||
tokens.fromGroup
|
||||
} else {
|
||||
tokens.toGroup
|
||||
}.available.firstOrNull { it.currencyStatus.currency.id.value == id }?.currencyStatus to null
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToCoinBalanceUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
coin: CryptoCurrency.Coin,
|
||||
|
|
@ -875,34 +1023,65 @@ internal class SwapModel @Inject constructor(
|
|||
) {
|
||||
Timber.d("Subscribe to ${coin.id} balance updates")
|
||||
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = coin.id,
|
||||
isSingleWalletWithTokens = false,
|
||||
)
|
||||
.mapNotNull { (it as? Either.Right)?.value }
|
||||
.distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes
|
||||
.onEach {
|
||||
Timber.d("${coin.id} balance is ${it.value.amount}")
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
getAccountCurrencyStatusUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = coin,
|
||||
).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes
|
||||
.onEach { (account, currencyStatus) ->
|
||||
Timber.d("${coin.id} balance is ${currencyStatus.value.amount}")
|
||||
|
||||
dataState = dataState.copy(
|
||||
feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = it,
|
||||
).getOrNull() ?: it,
|
||||
)
|
||||
dataState = dataState.copy(
|
||||
feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = currencyStatus,
|
||||
).getOrNull() ?: currencyStatus,
|
||||
)
|
||||
|
||||
uiState = if (isFromCurrency) {
|
||||
dataState = dataState.copy(fromCryptoCurrency = it)
|
||||
stateBuilder.updateSendCurrencyBalance(uiState, it)
|
||||
} else {
|
||||
dataState = dataState.copy(toCryptoCurrency = it)
|
||||
stateBuilder.updateReceiveCurrencyBalance(uiState, it)
|
||||
uiState = if (isFromCurrency) {
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = currencyStatus,
|
||||
fromAccount = account,
|
||||
)
|
||||
stateBuilder.updateSendCurrencyBalance(uiState, currencyStatus)
|
||||
} else {
|
||||
dataState = dataState.copy(
|
||||
toCryptoCurrency = currencyStatus,
|
||||
toAccount = account,
|
||||
)
|
||||
stateBuilder.updateReceiveCurrencyBalance(uiState, currencyStatus)
|
||||
}
|
||||
|
||||
startLoadingQuotesFromLastState(isSilent = true)
|
||||
}
|
||||
} else {
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = coin.id,
|
||||
isSingleWalletWithTokens = false,
|
||||
).mapNotNull { (it as? Either.Right)?.value }
|
||||
.distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes
|
||||
.onEach {
|
||||
Timber.d("${coin.id} balance is ${it.value.amount}")
|
||||
|
||||
startLoadingQuotesFromLastState(isSilent = true)
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
dataState = dataState.copy(
|
||||
feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = it,
|
||||
).getOrNull() ?: it,
|
||||
)
|
||||
|
||||
uiState = if (isFromCurrency) {
|
||||
dataState = dataState.copy(fromCryptoCurrency = it)
|
||||
stateBuilder.updateSendCurrencyBalance(uiState, it)
|
||||
} else {
|
||||
dataState = dataState.copy(toCryptoCurrency = it)
|
||||
stateBuilder.updateReceiveCurrencyBalance(uiState, it)
|
||||
}
|
||||
|
||||
startLoadingQuotesFromLastState(isSilent = true)
|
||||
}
|
||||
}.flowOn(dispatchers.main)
|
||||
.launchIn(modelScope)
|
||||
.saveIn(if (isFromCurrency) fromTokenBalanceJobHolder else toTokenBalanceJobHolder)
|
||||
}
|
||||
|
|
@ -910,14 +1089,18 @@ internal class SwapModel @Inject constructor(
|
|||
private fun onChangeCardsClicked() {
|
||||
modelScope.launch {
|
||||
val newFromToken = dataState.toCryptoCurrency
|
||||
val newFromAccount = dataState.toAccount
|
||||
val newToToken = dataState.fromCryptoCurrency
|
||||
val newToAccount = dataState.fromAccount
|
||||
|
||||
if (newFromToken != null && newToToken != null) {
|
||||
isAmountChangedByUser = true
|
||||
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = newFromToken,
|
||||
fromAccount = newFromAccount,
|
||||
toCryptoCurrency = newToToken,
|
||||
toAccount = newToAccount,
|
||||
)
|
||||
isOrderReversed = !isOrderReversed
|
||||
dataState.tokensDataState?.let {
|
||||
|
|
@ -940,7 +1123,9 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
startLoadingQuotes(
|
||||
fromToken = newFromToken,
|
||||
fromAccount = newFromAccount,
|
||||
toToken = newToToken,
|
||||
toAccount = newToAccount,
|
||||
amount = lastAmount.value,
|
||||
reduceBalanceBy = lastReducedBalanceBy.value,
|
||||
toProvidersList = findSwapProviders(newFromToken, newToToken),
|
||||
|
|
@ -982,7 +1167,9 @@ internal class SwapModel @Inject constructor(
|
|||
amountDebouncer.debounce(modelScope, DEBOUNCE_AMOUNT_DELAY, forceUpdate = forceQuotesUpdate) {
|
||||
startLoadingQuotes(
|
||||
fromToken = fromToken,
|
||||
fromAccount = dataState.fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = dataState.toAccount,
|
||||
amount = lastAmount.value,
|
||||
reduceBalanceBy = lastReducedBalanceBy.value,
|
||||
toProvidersList = findSwapProviders(fromToken, toToken),
|
||||
|
|
@ -1267,7 +1454,14 @@ internal class SwapModel @Inject constructor(
|
|||
toToken.currency.id.value
|
||||
}
|
||||
|
||||
return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers.orEmpty()
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
groupToFind.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) ->
|
||||
currencyList.find { idToFind == it.cryptoCurrencyStatus.currency.id.value && it.isAvailable }
|
||||
}?.providers
|
||||
} else {
|
||||
groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }
|
||||
?.providers
|
||||
}.orEmpty()
|
||||
}
|
||||
|
||||
private fun Map<SwapProvider, SwapState>.getLastLoadedSuccessStates(): SuccessLoadedSwapData {
|
||||
|
|
@ -1293,9 +1487,11 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
val chosen = if (isOrderReversed) from else to
|
||||
|
||||
return currenciesGroup.available
|
||||
.map { it.currencyStatus.currency }
|
||||
.contains(chosen.currency)
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
currenciesGroup.accountCurrencyList.flatMap { it.currencyList.map { it.cryptoCurrencyStatus } }
|
||||
} else {
|
||||
currenciesGroup.available.map { it.currencyStatus }
|
||||
}.map { it.currency }.contains(chosen.currency)
|
||||
}
|
||||
|
||||
private fun sendPermissionApproveClickedEvent() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
|
|
@ -14,6 +15,8 @@ data class SwapProcessDataState(
|
|||
val fromCryptoCurrency: CryptoCurrencyStatus? = null,
|
||||
val toCryptoCurrency: CryptoCurrencyStatus? = null,
|
||||
val feePaidCryptoCurrency: CryptoCurrencyStatus? = null,
|
||||
val fromAccount: Account.CryptoPortfolio? = null,
|
||||
val toAccount: Account.CryptoPortfolio? = null,
|
||||
// Amount from input
|
||||
val amount: String? = null,
|
||||
val reduceBalanceBy: BigDecimal = BigDecimal.ZERO,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
data class SwapSelectTokenStateHolder(
|
||||
internal data class SwapSelectTokenStateHolder(
|
||||
val availableTokens: ImmutableList<TokenToSelectState>,
|
||||
val unavailableTokens: ImmutableList<TokenToSelectState>,
|
||||
val tokensListData: TokenListUMData,
|
||||
val afterSearch: Boolean,
|
||||
val onSearchEntered: (String) -> Unit,
|
||||
val onTokenSelected: (String) -> Unit,
|
||||
)
|
||||
|
||||
sealed class TokenToSelectState {
|
||||
internal sealed class TokenToSelectState {
|
||||
|
||||
data class Title(val title: TextReference) : TokenToSelectState()
|
||||
|
||||
|
|
@ -26,8 +29,25 @@ sealed class TokenToSelectState {
|
|||
) : TokenToSelectState()
|
||||
}
|
||||
|
||||
data class TokenBalanceData(
|
||||
internal data class TokenBalanceData(
|
||||
val amount: String?,
|
||||
val amountEquivalent: String?,
|
||||
val isBalanceHidden: Boolean,
|
||||
)
|
||||
)
|
||||
|
||||
internal sealed interface TokenListUMData {
|
||||
|
||||
val tokensList: ImmutableList<TokensListItemUM>
|
||||
|
||||
data class AccountList(
|
||||
override val tokensList: ImmutableList<TokensListItemUM.Portfolio>,
|
||||
) : TokenListUMData
|
||||
|
||||
data class TokenList(
|
||||
override val tokensList: ImmutableList<TokensListItemUM>,
|
||||
) : TokenListUMData
|
||||
|
||||
data object EmptyList : TokenListUMData {
|
||||
override val tokensList: ImmutableList<TokensListItemUM> = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
|
@ -14,32 +14,39 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerW2
|
||||
import com.tangem.core.ui.components.appbar.ExpandableSearchView
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.tokenlist.PortfolioListItem
|
||||
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
|
||||
import com.tangem.core.ui.components.tokenlist.TokenListItem
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||
import com.tangem.core.ui.utils.lazyListItemPosition
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.TokenBalanceData
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import com.tangem.feature.swap.models.TokenToSelectState
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) {
|
||||
internal fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) {
|
||||
BackHandler(onBack = onBack)
|
||||
|
||||
Scaffold(
|
||||
|
|
@ -49,10 +56,12 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit)
|
|||
content = { padding ->
|
||||
val modifier = Modifier.padding(padding)
|
||||
when {
|
||||
state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() && state.afterSearch -> {
|
||||
state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() &&
|
||||
state.tokensListData.tokensList.isEmpty() && state.afterSearch -> {
|
||||
TokensNotFound(modifier)
|
||||
}
|
||||
state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() && !state.afterSearch -> {
|
||||
state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() &&
|
||||
state.tokensListData.tokensList.isEmpty() && !state.afterSearch -> {
|
||||
EmptyTokensList(modifier)
|
||||
}
|
||||
else -> {
|
||||
|
|
@ -133,6 +142,22 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
|
|||
.imePadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
when (val list = state.tokensListData) {
|
||||
is TokenListUMData.AccountList -> list.tokensList.forEach { item ->
|
||||
portfolioTokensList(
|
||||
portfolio = item,
|
||||
isBalanceHidden = false, // state.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
is TokenListUMData.TokenList -> {
|
||||
tokensList(
|
||||
items = list.tokensList,
|
||||
isBalanceHidden = false, // state.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
TokenListUMData.EmptyList -> Unit
|
||||
}
|
||||
|
||||
tokensToSelectItems(state.availableTokens, state.onTokenSelected)
|
||||
|
||||
item { SpacerH12() }
|
||||
|
|
@ -143,6 +168,86 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
|
|||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.tokensList(items: ImmutableList<TokensListItemUM>, isBalanceHidden: Boolean) {
|
||||
itemsIndexed(
|
||||
items = items,
|
||||
key = { _, item -> item.id },
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { index, item ->
|
||||
TokenListItem(
|
||||
state = item,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = items.lastIndex,
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
|
||||
.semantics { lazyListItemPosition = index },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portfolio, isBalanceHidden: Boolean) {
|
||||
val tokens = portfolio.tokens
|
||||
val isExpanded = portfolio.isExpanded
|
||||
|
||||
portfolioItem(
|
||||
portfolio = portfolio,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
if (!isExpanded) return
|
||||
itemsIndexed(
|
||||
items = tokens,
|
||||
key = { _, item -> item.id },
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { tokenIndex, token ->
|
||||
val indexWithHeader = tokenIndex.inc()
|
||||
PortfolioTokensListItem(
|
||||
state = token,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = indexWithHeader,
|
||||
lastIndex = tokens.lastIndex.inc(),
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.conditional(tokenIndex == tokens.lastIndex) {
|
||||
Modifier.padding(bottom = 8.dp)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun LazyListScope.portfolioItem(
|
||||
portfolio: TokensListItemUM.Portfolio,
|
||||
modifier: Modifier,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
item(
|
||||
key = "account-${portfolio.id}",
|
||||
contentType = "account",
|
||||
) {
|
||||
PortfolioListItem(
|
||||
state = portfolio,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = 0,
|
||||
lastIndex = portfolio.tokens.lastIndex.inc(),
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.then(modifier),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.tokensToSelectItems(
|
||||
items: ImmutableList<TokenToSelectState>,
|
||||
onTokenClick: (String) -> Unit,
|
||||
|
|
@ -311,6 +416,7 @@ private fun TokenScreenPreview() {
|
|||
state = SwapSelectTokenStateHolder(
|
||||
availableTokens = listOf(title, token, token, token).toImmutableList(),
|
||||
unavailableTokens = listOf(title, token, token, token).toImmutableList(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
afterSearch = false,
|
||||
onSearchEntered = {},
|
||||
onTokenSelected = {},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue