Updated on 2026-08-14
This commit is contained in:
parent
9c296c7ced
commit
a9dbcbc0d1
14 changed files with 687 additions and 90 deletions
|
|
@ -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