Updated on 2026-08-14
This commit is contained in:
commit
31be70ecf3
529 changed files with 15730 additions and 4170 deletions
|
|
@ -0,0 +1,147 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
|
||||
import com.tangem.common.ui.tokens.TokenItemStateConverter
|
||||
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount
|
||||
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount
|
||||
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.ui.AccountSwapAvailability
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class AccountTokenItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val unavailableErrorText: TextReference,
|
||||
private val onItemClick: (String) -> Unit,
|
||||
) : Converter<AccountSwapAvailability, TokensListItemUM.Portfolio> {
|
||||
|
||||
override fun convert(value: AccountSwapAvailability): TokensListItemUM.Portfolio {
|
||||
return TokensListItemUM.Portfolio(
|
||||
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
account = value.account,
|
||||
onItemClick = null,
|
||||
).convert(TotalFiatBalance.Failed),
|
||||
isExpanded = true,
|
||||
isCollapsable = false,
|
||||
tokens = value.currencyList.map { accountSwapCurrency ->
|
||||
if (accountSwapCurrency.isAvailable) {
|
||||
createAvailableItemConverter()
|
||||
} else {
|
||||
createUnavailableItemConverter()
|
||||
}.convert(accountSwapCurrency.cryptoCurrencyStatus)
|
||||
}.map(TokensListItemUM::Token).toPersistentList(),
|
||||
)
|
||||
}
|
||||
|
||||
fun createAvailableItemConverter(): TokenItemStateConverter {
|
||||
return TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
subtitleStateProvider = {
|
||||
createSubtitleState(
|
||||
status = it,
|
||||
isAvailable = true,
|
||||
text = stringReference(value = it.currency.symbol),
|
||||
)
|
||||
},
|
||||
subtitle2StateProvider = ::createSubtitle2State,
|
||||
fiatAmountStateProvider = {
|
||||
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true)
|
||||
},
|
||||
onItemClick = { account, currencyStatus -> onItemClick(currencyStatus.currency.id.value) },
|
||||
)
|
||||
}
|
||||
|
||||
fun createUnavailableItemConverter(): TokenItemStateConverter {
|
||||
return TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) },
|
||||
titleStateProvider = {
|
||||
TokenItemState.TitleState.Content(
|
||||
text = stringReference(value = it.currency.name),
|
||||
isAvailable = false,
|
||||
)
|
||||
},
|
||||
subtitleStateProvider = {
|
||||
createSubtitleState(
|
||||
status = it,
|
||||
isAvailable = false,
|
||||
text = unavailableErrorText,
|
||||
)
|
||||
},
|
||||
subtitle2StateProvider = ::createSubtitle2State,
|
||||
fiatAmountStateProvider = {
|
||||
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSubtitleState(
|
||||
status: CryptoCurrencyStatus,
|
||||
isAvailable: Boolean,
|
||||
text: TextReference,
|
||||
): TokenItemState.SubtitleState {
|
||||
return when (status.value) {
|
||||
CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading
|
||||
else -> {
|
||||
TokenItemState.SubtitleState.TextContent(
|
||||
value = text,
|
||||
isAvailable = isAvailable,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSubtitle2State(status: CryptoCurrencyStatus): TokenItemState.Subtitle2State? {
|
||||
return when (status.value) {
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
-> {
|
||||
TokenItemState.Subtitle2State.TextContent(
|
||||
text = status.getFormattedCryptoAmount(),
|
||||
isFlickering = status.value.isFlickering(),
|
||||
)
|
||||
}
|
||||
is CryptoCurrencyStatus.Loading,
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFiatAmountStateProvider(
|
||||
status: CryptoCurrencyStatus,
|
||||
appCurrency: AppCurrency,
|
||||
isAvailable: Boolean,
|
||||
): TokenItemState.FiatAmountState? {
|
||||
return when (status.value) {
|
||||
is CryptoCurrencyStatus.Loaded,
|
||||
is CryptoCurrencyStatus.Custom,
|
||||
is CryptoCurrencyStatus.NoQuote,
|
||||
is CryptoCurrencyStatus.NoAccount,
|
||||
-> {
|
||||
TokenItemState.FiatAmountState.TextContent(
|
||||
text = status.getFormattedFiatAmount(appCurrency = appCurrency),
|
||||
isAvailable = isAvailable,
|
||||
isFlickering = status.value.isFlickering(),
|
||||
)
|
||||
}
|
||||
is CryptoCurrencyStatus.Unreachable,
|
||||
is CryptoCurrencyStatus.NoAmount,
|
||||
is CryptoCurrencyStatus.MissedDerivation,
|
||||
is CryptoCurrencyStatus.Loading,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,8 +54,10 @@ class TokensDataConverter(
|
|||
}
|
||||
}
|
||||
.toImmutableList(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
onSearchEntered = onSearchEntered,
|
||||
onTokenSelected = onTokenSelected,
|
||||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
afterSearch = group.isAfterSearch,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.TokenListUMData
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.plus
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class TokensDataConverterV2(
|
||||
private val onSearchEntered: (String) -> Unit,
|
||||
private val onTokenSelected: (String) -> Unit,
|
||||
private val tokensDataState: CurrenciesGroup,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val isAccountsMode: Boolean,
|
||||
appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Transformer<SwapStateHolder> {
|
||||
|
||||
private val accountListItemConverter = AccountTokenItemConverter(
|
||||
appCurrency = appCurrencyProvider(),
|
||||
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
|
||||
onItemClick = onTokenSelected,
|
||||
)
|
||||
|
||||
override fun transform(prevState: SwapStateHolder): SwapStateHolder {
|
||||
val accountList = tokensDataState.accountCurrencyList
|
||||
return prevState.copy(
|
||||
selectTokenState = SwapSelectTokenStateHolder(
|
||||
availableTokens = persistentListOf(),
|
||||
unavailableTokens = persistentListOf(),
|
||||
tokensListData = if (isAccountsMode) {
|
||||
TokenListUMData.AccountList(
|
||||
tokensList = accountListItemConverter.convertList(accountList).toPersistentList(),
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.TokenList(
|
||||
tokensList = persistentListOf(
|
||||
TokensListItemUM.GroupTitle(
|
||||
id = "available_tokens_title",
|
||||
text = resourceReference(R.string.exchange_tokens_available_tokens_header),
|
||||
),
|
||||
) + accountList.flatMap { (_, currencyList) ->
|
||||
currencyList.asSequence().map { accountSwapCurrency ->
|
||||
if (accountSwapCurrency.isAvailable) {
|
||||
accountListItemConverter.createAvailableItemConverter()
|
||||
} else {
|
||||
accountListItemConverter.createUnavailableItemConverter()
|
||||
}.convert(accountSwapCurrency.cryptoCurrencyStatus)
|
||||
}.map(TokensListItemUM::Token).toPersistentList()
|
||||
}.toPersistentList(),
|
||||
)
|
||||
},
|
||||
onSearchEntered = onSearchEntered,
|
||||
onTokenSelected = onTokenSelected,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
afterSearch = tokensDataState.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>()
|
||||
|
|
@ -133,6 +141,7 @@ internal class SwapModel @Inject constructor(
|
|||
actions = createUiActions(),
|
||||
isBalanceHiddenProvider = Provider { isBalanceHidden },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
isAccountsModeProvider = Provider { isAccountsMode },
|
||||
)
|
||||
|
||||
private val inputNumberFormatter =
|
||||
|
|
@ -158,6 +167,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 +196,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 +267,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 +280,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 +330,7 @@ internal class SwapModel @Inject constructor(
|
|||
applyInitialTokenChoice(
|
||||
state = TokensDataStateExpress.EMPTY,
|
||||
selectedCurrency = null,
|
||||
selectedAccount = null,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
|
||||
|
|
@ -304,6 +367,7 @@ internal class SwapModel @Inject constructor(
|
|||
private fun applyInitialTokenChoice(
|
||||
state: TokensDataStateExpress,
|
||||
selectedCurrency: CryptoCurrencyStatus?,
|
||||
selectedAccount: Account.CryptoPortfolio?,
|
||||
isReverseFromTo: Boolean,
|
||||
) {
|
||||
// exceptional case
|
||||
|
|
@ -321,14 +385,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 +414,27 @@ 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,
|
||||
)
|
||||
} 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,21 @@ 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,
|
||||
fromAccount = fromAccount,
|
||||
toAccount = toAccount,
|
||||
mainTokenId = initialCurrencyFrom.id.value,
|
||||
)
|
||||
}
|
||||
singleTaskScheduler.scheduleTask(
|
||||
modelScope,
|
||||
loadQuotesTask(
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
amount = amount,
|
||||
reduceBalanceBy = reduceBalanceBy,
|
||||
toProvidersList = toProvidersList,
|
||||
|
|
@ -380,7 +472,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 +485,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 +505,9 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
swapInteractor.findBestQuote(
|
||||
fromToken = fromToken,
|
||||
fromAccount = fromAccount,
|
||||
toToken = toToken,
|
||||
toAccount = toAccount,
|
||||
providers = toProvidersList,
|
||||
amountToSwap = amount,
|
||||
reduceBalanceBy = reduceBalanceBy,
|
||||
|
|
@ -475,6 +573,7 @@ internal class SwapModel @Inject constructor(
|
|||
fromTokenStatus = fromToken,
|
||||
toTokenStatus = toTokenStatus,
|
||||
isReverseSwapPossible = isReverseSwapPossible(),
|
||||
toAccount = dataState.toAccount,
|
||||
)
|
||||
}
|
||||
is SwapState.SwapError -> {
|
||||
|
|
@ -488,6 +587,7 @@ internal class SwapModel @Inject constructor(
|
|||
includeFeeInAmount = state.includeFeeInAmount,
|
||||
isReverseSwapPossible = isReverseSwapPossible(),
|
||||
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
|
||||
toAccount = dataState.toAccount,
|
||||
)
|
||||
sendErrorAnalyticsEvent(state.error, provider)
|
||||
}
|
||||
|
|
@ -774,6 +874,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 +883,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 +913,7 @@ internal class SwapModel @Inject constructor(
|
|||
toGroup = tokenDataState.toGroup.copy(
|
||||
available = available,
|
||||
unavailable = unavailable,
|
||||
accountCurrencyList = accountCurrencyList,
|
||||
isAfterSearch = true,
|
||||
),
|
||||
)
|
||||
|
|
@ -805,25 +924,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 +955,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 +979,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 +998,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 +1027,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 +1093,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 {
|
||||
|
|
@ -937,10 +1124,13 @@ internal class SwapModel @Inject constructor(
|
|||
amountRaw = lastAmount.value,
|
||||
fromToken = newFromToken.currency,
|
||||
minTxAmount = minTxAmount,
|
||||
fromAccount = dataState.fromAccount,
|
||||
)
|
||||
startLoadingQuotes(
|
||||
fromToken = newFromToken,
|
||||
fromAccount = newFromAccount,
|
||||
toToken = newToToken,
|
||||
toAccount = newToAccount,
|
||||
amount = lastAmount.value,
|
||||
reduceBalanceBy = lastReducedBalanceBy.value,
|
||||
toProvidersList = findSwapProviders(newFromToken, newToToken),
|
||||
|
|
@ -972,6 +1162,7 @@ internal class SwapModel @Inject constructor(
|
|||
amountRaw = lastAmount.value,
|
||||
fromToken = fromToken.currency,
|
||||
minTxAmount = minTxAmount,
|
||||
fromAccount = dataState.fromAccount,
|
||||
)
|
||||
|
||||
if (toToken != null) {
|
||||
|
|
@ -982,7 +1173,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 +1460,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 +1493,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,22 @@
|
|||
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 isBalanceHidden: Boolean,
|
||||
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 +30,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()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,10 @@ package com.tangem.feature.swap.models
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
|
|
@ -83,21 +83,21 @@ data class SwapButton(
|
|||
|
||||
sealed interface TransactionCardType {
|
||||
|
||||
val header: TextReference
|
||||
val accountTitleUM: AccountTitleUM?
|
||||
val inputError: InputError
|
||||
|
||||
data class Inputtable(
|
||||
val onAmountChanged: ((String) -> Unit),
|
||||
val onFocusChanged: ((Boolean) -> Unit),
|
||||
override val inputError: InputError,
|
||||
override val header: TextReference = TextReference.Res(R.string.swapping_from_title),
|
||||
override val accountTitleUM: AccountTitleUM?,
|
||||
) : TransactionCardType
|
||||
|
||||
data class ReadOnly(
|
||||
val showWarning: Boolean = false,
|
||||
val onWarningClick: (() -> Unit)? = null,
|
||||
override val inputError: InputError = InputError.Empty,
|
||||
override val header: TextReference = TextReference.Res(R.string.swapping_to_title),
|
||||
override val accountTitleUM: AccountTitleUM? = null,
|
||||
) : TransactionCardType
|
||||
|
||||
sealed interface InputError {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
|
|
@ -12,6 +13,8 @@ data class SwapSuccessStateHolder(
|
|||
val providerName: TextReference,
|
||||
val providerType: TextReference,
|
||||
val providerIcon: String,
|
||||
val fromTitle: AccountTitleUM,
|
||||
val toTitle: AccountTitleUM,
|
||||
val fromTokenAmount: TextReference,
|
||||
val toTokenAmount: TextReference,
|
||||
val fromTokenFiatAmount: TextReference,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
package com.tangem.feature.swap.preview
|
||||
|
||||
import com.tangem.common.ui.account.AccountNameUM
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
||||
import com.tangem.feature.swap.models.SwapSuccessStateHolder
|
||||
|
||||
|
|
@ -14,6 +19,16 @@ internal data object SwapSuccessStatePreview {
|
|||
providerType = TextReference.Str(ExchangeProviderType.DEX.providerName),
|
||||
showStatusButton = false,
|
||||
providerIcon = "",
|
||||
fromTitle = AccountTitleUM.Account(
|
||||
prefixText = stringReference("From"),
|
||||
name = AccountNameUM.DefaultMain.value,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
|
||||
),
|
||||
toTitle = AccountTitleUM.Account(
|
||||
prefixText = stringReference("To"),
|
||||
name = AccountNameUM.DefaultMain.value,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
|
||||
),
|
||||
fromTokenAmount = TextReference.Str("1 000 DAI"),
|
||||
toTokenAmount = TextReference.Str("1 000 MATIC"),
|
||||
fromTokenFiatAmount = TextReference.Str("1 000 $"),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.tangem.feature.swap.ui
|
|||
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.*
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
|
|
@ -18,12 +20,14 @@ import com.tangem.core.ui.format.bigdecimal.fiat
|
|||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
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.UserWallet
|
||||
import com.tangem.domain.promo.models.StoryContent
|
||||
import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter
|
||||
import com.tangem.feature.swap.converters.TokensDataConverter
|
||||
import com.tangem.feature.swap.converters.TokensDataConverterV2
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
||||
|
|
@ -59,6 +63,7 @@ internal class StateBuilder(
|
|||
private val actions: UiActions,
|
||||
private val isBalanceHiddenProvider: Provider<Boolean>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val isAccountsModeProvider: Provider<Boolean>,
|
||||
) {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
|
@ -86,6 +91,7 @@ internal class StateBuilder(
|
|||
onAmountChanged = actions.onAmountChanged,
|
||||
onFocusChanged = actions.onAmountSelected,
|
||||
inputError = TransactionCardType.InputError.Empty,
|
||||
accountTitleUM = null,
|
||||
),
|
||||
amountEquivalent = null,
|
||||
amountTextFieldValue = null,
|
||||
|
|
@ -183,11 +189,14 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun createQuotesLoadingState(
|
||||
uiStateHolder: SwapStateHolder,
|
||||
fromToken: CryptoCurrency,
|
||||
toToken: CryptoCurrency,
|
||||
mainTokenId: String,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
): SwapStateHolder {
|
||||
val canSelectSendToken = mainTokenId != fromToken.id.value
|
||||
val canSelectReceiveToken = mainTokenId != toToken.id.value
|
||||
|
|
@ -199,7 +208,7 @@ internal class StateBuilder(
|
|||
} else {
|
||||
sendInputType.copy(
|
||||
inputError = TransactionCardType.InputError.Empty,
|
||||
header = TextReference.Res(R.string.swapping_from_title),
|
||||
accountTitleUM = getFromCardAccountTitle(fromAccount),
|
||||
)
|
||||
}
|
||||
return uiStateHolder.copy(
|
||||
|
|
@ -218,7 +227,9 @@ internal class StateBuilder(
|
|||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
),
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
type = TransactionCardType.ReadOnly(
|
||||
accountTitleUM = getToCardAccountTitle(toAccount),
|
||||
),
|
||||
amountTextFieldValue = null,
|
||||
amountEquivalent = null,
|
||||
token = uiStateHolder.receiveCardData.token,
|
||||
|
|
@ -280,10 +291,12 @@ internal class StateBuilder(
|
|||
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
|
||||
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
|
||||
val isInsufficientFunds = isInsufficientFundsCondition(quoteModel)
|
||||
val insufficientFundsHeader = if (isInsufficientFunds) {
|
||||
TextReference.Res(R.string.swapping_insufficient_funds)
|
||||
} else {
|
||||
TextReference.Res(R.string.swapping_from_title)
|
||||
val fromAccount = quoteModel.fromTokenInfo.account
|
||||
val toAccount = quoteModel.toTokenInfo.account
|
||||
|
||||
val fromAccountTitleUM = when {
|
||||
isInsufficientFunds -> AccountTitleUM.Text(TextReference.Res(R.string.swapping_insufficient_funds))
|
||||
else -> getFromCardAccountTitle(fromAccount)
|
||||
}
|
||||
val sendCardType = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable)
|
||||
val sendInput = when (sendCardType.inputError) {
|
||||
|
|
@ -299,7 +312,7 @@ internal class StateBuilder(
|
|||
}
|
||||
sendCardType.copy(
|
||||
inputError = error,
|
||||
header = insufficientFundsHeader,
|
||||
accountTitleUM = fromAccountTitleUM,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -321,7 +334,8 @@ internal class StateBuilder(
|
|||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReadOnly(
|
||||
showWarning = true,
|
||||
actions.onReceiveCardWarningClick,
|
||||
onWarningClick = actions.onReceiveCardWarningClick,
|
||||
accountTitleUM = getToCardAccountTitle(toAccount),
|
||||
),
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
quoteModel.toTokenInfo.tokenAmount
|
||||
|
|
@ -423,6 +437,7 @@ internal class StateBuilder(
|
|||
swapProvider: SwapProvider,
|
||||
fromToken: TokenSwapInfo,
|
||||
toToken: CryptoCurrencyStatus?,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
includeFeeInAmount: IncludeFeeInAmount,
|
||||
expressDataError: ExpressDataError,
|
||||
isReverseSwapPossible: Boolean,
|
||||
|
|
@ -445,9 +460,10 @@ internal class StateBuilder(
|
|||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
)
|
||||
val type = TransactionCardType.ReadOnly(accountTitleUM = getToCardAccountTitle(toAccount))
|
||||
val receiveCardData = toToken?.let {
|
||||
SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
type = type,
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
),
|
||||
|
|
@ -463,7 +479,7 @@ internal class StateBuilder(
|
|||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
)
|
||||
} ?: SwapCardState.Empty(
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
type = type,
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
|
|
@ -535,11 +551,13 @@ internal class StateBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun createQuotesEmptyAmountState(
|
||||
uiStateHolder: SwapStateHolder,
|
||||
emptyAmountState: SwapState.EmptyAmountState,
|
||||
fromTokenStatus: CryptoCurrencyStatus,
|
||||
toTokenStatus: CryptoCurrencyStatus?,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
isReverseSwapPossible: Boolean,
|
||||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
|
|
@ -560,7 +578,9 @@ internal class StateBuilder(
|
|||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
),
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
type = TransactionCardType.ReadOnly(
|
||||
accountTitleUM = getToCardAccountTitle(toAccount),
|
||||
),
|
||||
amountTextFieldValue = TextFieldValue("0"),
|
||||
amountEquivalent = emptyAmountState.zeroAmountEquivalent,
|
||||
token = uiStateHolder.receiveCardData.token,
|
||||
|
|
@ -614,17 +634,34 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
|
||||
fun addTokensToStateV2(
|
||||
uiState: SwapStateHolder,
|
||||
tokensDataState: CurrenciesGroup,
|
||||
isAccountsMode: Boolean,
|
||||
): SwapStateHolder {
|
||||
return TokensDataConverterV2(
|
||||
onSearchEntered = actions.onSearchEntered,
|
||||
onTokenSelected = actions.onTokenSelected,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
tokensDataState = tokensDataState,
|
||||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
isAccountsMode = isAccountsMode,
|
||||
).transform(uiState)
|
||||
}
|
||||
|
||||
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun updateSwapAmount(
|
||||
uiState: SwapStateHolder,
|
||||
amountFormatted: String,
|
||||
amountRaw: String,
|
||||
fromToken: CryptoCurrency,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
minTxAmount: BigDecimal?,
|
||||
): SwapStateHolder {
|
||||
if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState
|
||||
|
|
@ -635,12 +672,14 @@ internal class StateBuilder(
|
|||
}
|
||||
(uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy(
|
||||
inputError = TransactionCardType.InputError.WrongAmount,
|
||||
header = resourceReference(R.string.transfer_min_amount_error, wrappedList(minAmountFormatted)),
|
||||
accountTitleUM = AccountTitleUM.Text(
|
||||
resourceReference(R.string.transfer_min_amount_error, wrappedList(minAmountFormatted)),
|
||||
),
|
||||
) ?: uiState.sendCardData.type
|
||||
} else {
|
||||
(uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy(
|
||||
inputError = TransactionCardType.InputError.Empty,
|
||||
header = TextReference.Res(R.string.swapping_from_title),
|
||||
accountTitleUM = getFromCardAccountTitle(fromAccount),
|
||||
) ?: uiState.sendCardData.type
|
||||
}
|
||||
return uiState.copy(
|
||||
|
|
@ -692,6 +731,7 @@ internal class StateBuilder(
|
|||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
val selectTokenState = uiState.selectTokenState?.copy(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
availableTokens = uiState.selectTokenState.availableTokens.map {
|
||||
when (it) {
|
||||
is TokenToSelectState.TokenToSelect -> {
|
||||
|
|
@ -811,6 +851,8 @@ internal class StateBuilder(
|
|||
providerIcon = providerState.iconUrl,
|
||||
rate = providerState.subtitle,
|
||||
fee = stringReference("${fee.feeCryptoFormattedWithNative} (${fee.feeFiatFormattedWithNative})"),
|
||||
fromTitle = getFromCardAccountTitle(fromAccount = dataState.fromAccount),
|
||||
toTitle = getToCardAccountTitle(toAccount = dataState.toAccount),
|
||||
fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()),
|
||||
toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()),
|
||||
fromTokenFiatAmount = stringReference(fromFiatAmount),
|
||||
|
|
@ -1327,6 +1369,30 @@ internal class StateBuilder(
|
|||
return FCA_RESTRICTED_PROVIDER_IDS.contains(providerId)
|
||||
}
|
||||
|
||||
private fun getFromCardAccountTitle(fromAccount: Account.CryptoPortfolio?): AccountTitleUM {
|
||||
return if (fromAccount != null && isAccountsModeProvider()) {
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.common_from),
|
||||
name = fromAccount.accountName.toUM().value,
|
||||
icon = fromAccount.icon.toUM(),
|
||||
)
|
||||
} else {
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_from_title))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getToCardAccountTitle(toAccount: Account.CryptoPortfolio?): AccountTitleUM {
|
||||
return if (toAccount != null && isAccountsModeProvider()) {
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.common_to),
|
||||
name = toAccount.accountName.toUM().value,
|
||||
icon = toAccount.icon.toUM(),
|
||||
)
|
||||
} else {
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_to_title))
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val RU_LOCALE = "ru"
|
||||
private const val EN_LOCALE = "en"
|
||||
|
|
|
|||
|
|
@ -383,7 +383,12 @@ private fun MainButton(state: SwapStateHolder) {
|
|||
// region preview
|
||||
|
||||
private val sendCard = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.Inputtable({}, {}, TransactionCardType.InputError.Empty),
|
||||
type = TransactionCardType.Inputtable(
|
||||
onAmountChanged = {},
|
||||
onFocusChanged = {},
|
||||
inputError = TransactionCardType.InputError.Empty,
|
||||
accountTitleUM = null,
|
||||
),
|
||||
amountTextFieldValue = TextFieldValue(),
|
||||
amountEquivalent = "1 000 000",
|
||||
tokenIconUrl = "",
|
||||
|
|
|
|||
|
|
@ -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 = state.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
is TokenListUMData.TokenList -> {
|
||||
tokensList(
|
||||
items = list.tokensList,
|
||||
isBalanceHidden = 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,7 +416,9 @@ private fun TokenScreenPreview() {
|
|||
state = SwapSelectTokenStateHolder(
|
||||
availableTokens = listOf(title, token, token, token).toImmutableList(),
|
||||
unavailableTokens = listOf(title, token, token, token).toImmutableList(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
afterSearch = false,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
onTokenSelected = {},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,21 +5,23 @@ import androidx.annotation.StringRes
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.account.AccountTitle
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.inputrow.InputRowBestRate
|
||||
import com.tangem.core.ui.components.inputrow.InputRowDefault
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImage
|
||||
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
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.utils.DateTimeFormatters
|
||||
|
|
@ -75,26 +77,18 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad
|
|||
),
|
||||
)
|
||||
SpacerH16()
|
||||
InputRowImage(
|
||||
title = TextReference.Res(R.string.swapping_from_title),
|
||||
SwapAmountBlock(
|
||||
title = state.fromTitle,
|
||||
subtitle = state.fromTokenAmount,
|
||||
caption = state.fromTokenFiatAmount,
|
||||
tokenIconState = state.fromTokenIconState ?: CurrencyIconState.Loading,
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
showNetworkIcon = true,
|
||||
tokenIconState = state.fromTokenIconState,
|
||||
)
|
||||
SpacerH16()
|
||||
InputRowImage(
|
||||
title = TextReference.Res(R.string.swapping_to_title),
|
||||
SwapAmountBlock(
|
||||
title = state.toTitle,
|
||||
subtitle = state.toTokenAmount,
|
||||
caption = state.toTokenFiatAmount,
|
||||
tokenIconState = state.toTokenIconState ?: CurrencyIconState.Loading,
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
showNetworkIcon = true,
|
||||
tokenIconState = state.toTokenIconState,
|
||||
)
|
||||
SpacerH16()
|
||||
InputRowBestRate(
|
||||
|
|
@ -117,6 +111,48 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwapAmountBlock(
|
||||
title: AccountTitleUM,
|
||||
subtitle: TextReference,
|
||||
caption: TextReference,
|
||||
tokenIconState: CurrencyIconState?,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
) {
|
||||
AccountTitle(title)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
state = tokenIconState ?: CurrencyIconState.Loading,
|
||||
shouldDisplayNetwork = true,
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = caption.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun SwapSuccessScreenButtons(
|
||||
|
|
|
|||
|
|
@ -34,14 +34,20 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.common.ui.account.AccountNameUM
|
||||
import com.tangem.common.ui.account.AccountTitle
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SwapTokenScreenTestTags
|
||||
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
import com.tangem.feature.swap.models.TransactionCardType
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -193,14 +199,22 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
|
|||
} else {
|
||||
TangemTheme.colors.text.warning
|
||||
}
|
||||
Text(
|
||||
text = type.header.resolveReference(),
|
||||
color = titleColor,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically),
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = type.accountTitleUM,
|
||||
label = "",
|
||||
) { currentAccountTitle ->
|
||||
if (currentAccountTitle != null) {
|
||||
AccountTitle(
|
||||
accountTitleUM = currentAccountTitle,
|
||||
textColor = titleColor,
|
||||
)
|
||||
} else {
|
||||
TextShimmer(
|
||||
text = stringResourceSafe(R.string.swapping_to_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerW16()
|
||||
if (balance.isNotBlank()) {
|
||||
AnimatedContent(targetState = balance, label = "") {
|
||||
|
|
@ -559,7 +573,12 @@ private fun Preview_TransactionCardWithoutPriceImpact_InDarkTheme() {
|
|||
@Composable
|
||||
private fun TransactionCardPreview() {
|
||||
TransactionCard(
|
||||
type = TransactionCardType.Inputtable({}, {}, TransactionCardType.InputError.Empty),
|
||||
type = TransactionCardType.Inputtable(
|
||||
onAmountChanged = {},
|
||||
onFocusChanged = {},
|
||||
inputError = TransactionCardType.InputError.Empty,
|
||||
accountTitleUM = null,
|
||||
),
|
||||
amountEquivalent = "1 000 000",
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "DAI",
|
||||
|
|
@ -575,7 +594,14 @@ private fun TransactionCardPreview() {
|
|||
@Suppress("MagicNumber")
|
||||
private fun TransactionCardPreviewWithPriceImpact() {
|
||||
TransactionCard(
|
||||
type = TransactionCardType.ReadOnly(showWarning = true),
|
||||
type = TransactionCardType.ReadOnly(
|
||||
showWarning = true,
|
||||
accountTitleUM = AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.common_from),
|
||||
name = AccountNameUM.DefaultMain.value,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
|
||||
),
|
||||
),
|
||||
amountEquivalent = "1 000 000",
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "DAI",
|
||||
|
|
@ -591,7 +617,13 @@ private fun TransactionCardPreviewWithPriceImpact() {
|
|||
@Suppress("MagicNumber")
|
||||
private fun TransactionCardPreviewWithoutPriceImpact() {
|
||||
TransactionCard(
|
||||
type = TransactionCardType.ReadOnly(),
|
||||
type = TransactionCardType.ReadOnly(
|
||||
accountTitleUM = AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.common_from),
|
||||
name = AccountNameUM.DefaultMain.value,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
|
||||
),
|
||||
),
|
||||
amountEquivalent = "1 000 000",
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "DAI",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue