Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-23 17:13:12 +05:00
parent 9c296c7ced
commit a9dbcbc0d1
14 changed files with 687 additions and 90 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = {},