Updated on 2026-08-14
This commit is contained in:
parent
3c5c53ce92
commit
537543636b
19 changed files with 431 additions and 145 deletions
|
|
@ -1,12 +1,9 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
|
||||
|
||||
interface BlockchainInteractor {
|
||||
|
||||
fun getTokenDecimals(token: CryptoCurrency): Int
|
||||
|
||||
/**
|
||||
* In app blockchain id, actual in blockchain sdk, not the same as networkId
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
|
||||
import com.tangem.lib.crypto.TransactionManager
|
||||
import javax.inject.Inject
|
||||
|
|
@ -22,12 +21,4 @@ internal class DefaultBlockchainInteractor @Inject constructor(
|
|||
override fun getExplorerTransactionLink(networkId: String, txAddress: String): String {
|
||||
return transactionManager.getExplorerTransactionLink(networkId, txAddress)
|
||||
}
|
||||
|
||||
override fun getTokenDecimals(token: CryptoCurrency): Int {
|
||||
return if (token is CryptoCurrency.Token) {
|
||||
token.decimals
|
||||
} else {
|
||||
transactionManager.getNativeTokenDecimals(token.network.backendId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.swap.domain
|
|||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
|
|
@ -110,4 +111,6 @@ interface SwapInteractor {
|
|||
networkId: String,
|
||||
fromToken: CryptoCurrency,
|
||||
): Boolean
|
||||
|
||||
fun getSelectedWallet(): UserWallet?
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.swap.domain.cache.SwapDataCache
|
||||
import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter
|
||||
|
|
@ -93,6 +94,10 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun getSelectedWallet(): UserWallet? {
|
||||
return getSelectedWalletSyncUseCase().getOrNull()
|
||||
}
|
||||
|
||||
private fun getToCurrenciesGroup(
|
||||
currency: CryptoCurrency,
|
||||
leastPairs: List<SwapPairLeast>,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.feature.swap.domain.di
|
||||
|
||||
import com.tangem.domain.tokens.GetCardTokensListUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
|
|
@ -66,13 +65,6 @@ class SwapDomainModule {
|
|||
return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder)
|
||||
}
|
||||
|
||||
@SwapScope
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
|
||||
return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
|
||||
}
|
||||
|
||||
@SwapScope
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ dependencies {
|
|||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
|
|||
|
|
@ -32,13 +32,17 @@ class TokensDataConverter(
|
|||
availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) }
|
||||
.toMutableList()
|
||||
.apply {
|
||||
this.add(0, availableTitle)
|
||||
if (this.isNotEmpty()) {
|
||||
this.add(0, availableTitle)
|
||||
}
|
||||
}
|
||||
.toImmutableList(),
|
||||
unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) }
|
||||
.toMutableList()
|
||||
.apply {
|
||||
this.add(0, unavailableTitle)
|
||||
if (this.isNotEmpty()) {
|
||||
this.add(0, unavailableTitle)
|
||||
}
|
||||
}
|
||||
.toImmutableList(),
|
||||
onSearchEntered = onSearchEntered,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.feature.swap.di
|
||||
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.feature.swap.domain.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
class SwapPresentationModule {
|
||||
|
||||
@ViewModelScoped
|
||||
@Provides
|
||||
fun providesGetCryptoCurrenciesUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
dispatcherProvider: CoroutineDispatcherProvider,
|
||||
quotesRepository: QuotesRepository,
|
||||
networksRepository: NetworksRepository,
|
||||
): GetCryptoCurrencyStatusSyncUseCase {
|
||||
return GetCryptoCurrencyStatusSyncUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
dispatchers = dispatcherProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import com.tangem.core.ui.components.states.SelectableItemsState
|
|||
import com.tangem.feature.swap.domain.models.ui.TxFee
|
||||
|
||||
data class SwapStateHolder(
|
||||
val sendCardData: SwapCardData,
|
||||
val receiveCardData: SwapCardData,
|
||||
val sendCardData: SwapCardState,
|
||||
val receiveCardData: SwapCardState,
|
||||
val networkCurrency: String,
|
||||
val networkId: String,
|
||||
val blockchainId: String, // not the same as networkId, its local id in app
|
||||
|
|
@ -33,18 +33,28 @@ data class SwapStateHolder(
|
|||
val onCancelPermissionBottomSheet: () -> Unit = {},
|
||||
)
|
||||
|
||||
data class SwapCardData(
|
||||
val type: TransactionCardType,
|
||||
val amountEquivalent: String?,
|
||||
val coinId: String?,
|
||||
val amountTextFieldValue: TextFieldValue?,
|
||||
val tokenIconUrl: String?,
|
||||
val tokenCurrency: String,
|
||||
val balance: String,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isNotNativeToken: Boolean,
|
||||
val canSelectAnotherToken: Boolean = false,
|
||||
)
|
||||
sealed class SwapCardState {
|
||||
|
||||
data class SwapCardData(
|
||||
val type: TransactionCardType,
|
||||
val amountEquivalent: String?,
|
||||
val coinId: String?,
|
||||
val amountTextFieldValue: TextFieldValue?,
|
||||
val tokenIconUrl: String?,
|
||||
val tokenCurrency: String,
|
||||
val balance: String,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isNotNativeToken: Boolean,
|
||||
val canSelectAnotherToken: Boolean = false,
|
||||
) : SwapCardState()
|
||||
|
||||
data class Empty(
|
||||
val type: TransactionCardType,
|
||||
val amountEquivalent: String?,
|
||||
val amountTextFieldValue: TextFieldValue?,
|
||||
val canSelectAnotherToken: Boolean = false,
|
||||
) : SwapCardState()
|
||||
}
|
||||
|
||||
data class SwapButton(
|
||||
val enabled: Boolean,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@ import com.tangem.core.ui.components.states.Item
|
|||
import com.tangem.core.ui.components.states.SelectableItemsState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.converters.TokensDataConverter
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
|
||||
|
|
@ -28,7 +31,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
internal class StateBuilder(
|
||||
private val actions: UiActions,
|
||||
private val isBalanceHiddenProvider: Provider<Boolean>,
|
||||
appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) {
|
||||
|
||||
private val tokensDataConverter = TokensDataConverter(
|
||||
|
|
@ -42,7 +45,7 @@ internal class StateBuilder(
|
|||
return SwapStateHolder(
|
||||
networkId = initialCurrency.network.backendId,
|
||||
blockchainId = networkInfo.blockchainId,
|
||||
sendCardData = SwapCardData(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.SendCard(actions.onAmountChanged, actions.onAmountSelected),
|
||||
amountEquivalent = null,
|
||||
amountTextFieldValue = null,
|
||||
|
|
@ -54,7 +57,7 @@ internal class StateBuilder(
|
|||
balance = "",
|
||||
isBalanceHidden = true,
|
||||
),
|
||||
receiveCardData = SwapCardData(
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
amountEquivalent = null,
|
||||
tokenIconUrl = "",
|
||||
|
|
@ -79,6 +82,53 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
|
||||
fun createNoAvailableTokensToSwapState(
|
||||
uiStateHolder: SwapStateHolder,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
),
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
|
||||
coinId = uiStateHolder.sendCardData.coinId,
|
||||
isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken,
|
||||
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
|
||||
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
|
||||
balance = fromToken.getFormattedAmount(),
|
||||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
),
|
||||
receiveCardData = SwapCardState.Empty(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = "0",
|
||||
),
|
||||
canSelectAnotherToken = true,
|
||||
),
|
||||
warnings = listOf(
|
||||
SwapWarning.NoAvailableTokensToSwap(
|
||||
notificationConfig = NotificationConfig(
|
||||
title = stringReference("No tokens"),
|
||||
subtitle = stringReference("Swap tokens not available"),
|
||||
iconResId = R.drawable.ic_alert_24,
|
||||
),
|
||||
),
|
||||
),
|
||||
fee = FeeState.Empty,
|
||||
swapButton = SwapButton(
|
||||
enabled = false,
|
||||
loading = false,
|
||||
onClick = { },
|
||||
),
|
||||
updateInProgress = false,
|
||||
)
|
||||
}
|
||||
|
||||
fun createQuotesLoadingState(
|
||||
uiStateHolder: SwapStateHolder,
|
||||
fromToken: CryptoCurrency,
|
||||
|
|
@ -87,8 +137,10 @@ internal class StateBuilder(
|
|||
): SwapStateHolder {
|
||||
val canSelectSendToken = mainTokenId != fromToken.id.value // TODO look at id matching
|
||||
val canSelectReceiveToken = mainTokenId != toToken.id.value // TODO look at id matching
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardData(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
|
||||
amountEquivalent = null,
|
||||
|
|
@ -100,7 +152,7 @@ internal class StateBuilder(
|
|||
balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "",
|
||||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
),
|
||||
receiveCardData = SwapCardData(
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
amountTextFieldValue = null,
|
||||
amountEquivalent = null,
|
||||
|
|
@ -128,12 +180,15 @@ internal class StateBuilder(
|
|||
* @param onFeeSetup callback for reset fee after auto update
|
||||
* @return updated whole screen state
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
fun createQuotesLoadedState(
|
||||
uiStateHolder: SwapStateHolder,
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
fromToken: CryptoCurrency,
|
||||
onFeeSetup: (TxFee) -> Unit,
|
||||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
val warnings = mutableListOf<SwapWarning>()
|
||||
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
|
||||
quoteModel.preparedSwapConfigState.isFeeEnough &&
|
||||
|
|
@ -159,7 +214,7 @@ internal class StateBuilder(
|
|||
}
|
||||
val feeState = createFeeState(quoteModel, uiStateHolder, onFeeSetup)
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardData(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
|
||||
amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance,
|
||||
|
|
@ -171,7 +226,7 @@ internal class StateBuilder(
|
|||
balance = quoteModel.fromTokenInfo.tokenWalletBalance,
|
||||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
),
|
||||
receiveCardData = SwapCardData(
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()),
|
||||
amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance,
|
||||
|
|
@ -208,8 +263,10 @@ internal class StateBuilder(
|
|||
uiStateHolder: SwapStateHolder,
|
||||
emptyAmountState: SwapState.EmptyAmountState,
|
||||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardData(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
|
||||
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
|
||||
amountEquivalent = emptyAmountState.zeroAmountEquivalent,
|
||||
|
|
@ -221,7 +278,7 @@ internal class StateBuilder(
|
|||
balance = emptyAmountState.fromTokenWalletBalance,
|
||||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
),
|
||||
receiveCardData = SwapCardData(
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
amountTextFieldValue = TextFieldValue("0"),
|
||||
amountEquivalent = emptyAmountState.zeroAmountEquivalent,
|
||||
|
|
@ -268,6 +325,7 @@ internal class StateBuilder(
|
|||
}
|
||||
|
||||
fun updateSwapAmount(uiState: SwapStateHolder, amount: String): SwapStateHolder {
|
||||
if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState
|
||||
return uiState.copy(
|
||||
sendCardData = uiState.sendCardData.copy(
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
|
|
@ -279,6 +337,8 @@ internal class StateBuilder(
|
|||
}
|
||||
|
||||
fun updateBalanceHiddenState(uiState: SwapStateHolder, isBalanceHidden: Boolean): SwapStateHolder {
|
||||
if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState
|
||||
if (uiState.receiveCardData !is SwapCardState.SwapCardData) return uiState
|
||||
val patchedSendCardData = uiState.sendCardData.copy(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
|
|
@ -666,11 +726,26 @@ internal class StateBuilder(
|
|||
return "$firstAddressPart...$secondAddressPart"
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
|
||||
val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN
|
||||
|
||||
return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String {
|
||||
val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN
|
||||
val appCurrency = appCurrencyProvider()
|
||||
|
||||
return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ADDRESS_MIN_LENGTH = 11
|
||||
const val ADDRESS_FIRST_PART_LENGTH = 7
|
||||
const val ADDRESS_SECOND_PART_LENGTH = 4
|
||||
private const val PRICE_IMPACT_THRESHOLD = 0.1
|
||||
private const val HUNDRED_PERCENTS = 100
|
||||
private const val UNKNOWN_AMOUNT_SIGN = "—"
|
||||
}
|
||||
}
|
||||
|
|
@ -145,48 +145,24 @@ private fun MainInfo(state: SwapStateHolder) {
|
|||
}
|
||||
val (topCard, bottomCard, button) = createRefs()
|
||||
val priceImpactWarning = state.warnings.filterIsInstance<SwapWarning.HighPriceImpact>().firstOrNull()
|
||||
TransactionCard(
|
||||
type = state.sendCardData.type,
|
||||
balance = if (state.sendCardData.isBalanceHidden) {
|
||||
STARS
|
||||
} else {
|
||||
state.sendCardData.balance
|
||||
},
|
||||
textFieldValue = state.sendCardData.amountTextFieldValue,
|
||||
amountEquivalent = state.sendCardData.amountEquivalent,
|
||||
tokenIconUrl = state.sendCardData.tokenIconUrl ?: "",
|
||||
tokenCurrency = state.sendCardData.tokenCurrency,
|
||||
priceImpact = priceImpactWarning,
|
||||
networkIconRes = if (state.sendCardData.isNotNativeToken) networkIconRes else null,
|
||||
iconPlaceholder = state.sendCardData.coinId?.let {
|
||||
getActiveIconResByCoinId(it)
|
||||
},
|
||||
onChangeTokenClick = if (state.sendCardData.canSelectAnotherToken) state.onSelectTokenClick else null,
|
||||
TransactionCardData(
|
||||
priceImpactWarning = priceImpactWarning,
|
||||
networkIconRes = networkIconRes,
|
||||
swapCardState = state.sendCardData,
|
||||
modifier = Modifier.constrainAs(topCard) {
|
||||
top.linkTo(parent.top)
|
||||
},
|
||||
onSelectTokenClick = state.onSelectTokenClick,
|
||||
)
|
||||
val marginCard = TangemTheme.dimens.spacing16
|
||||
TransactionCard(
|
||||
type = state.receiveCardData.type,
|
||||
balance = if (state.receiveCardData.isBalanceHidden) STARS else state.receiveCardData.balance,
|
||||
textFieldValue = state.receiveCardData.amountTextFieldValue,
|
||||
amountEquivalent = state.receiveCardData.amountEquivalent,
|
||||
tokenIconUrl = state.receiveCardData.tokenIconUrl ?: "",
|
||||
tokenCurrency = state.receiveCardData.tokenCurrency,
|
||||
priceImpact = priceImpactWarning,
|
||||
networkIconRes = if (state.receiveCardData.isNotNativeToken) networkIconRes else null,
|
||||
iconPlaceholder = state.receiveCardData.coinId?.let {
|
||||
getActiveIconResByCoinId(it)
|
||||
},
|
||||
onChangeTokenClick = if (state.receiveCardData.canSelectAnotherToken) {
|
||||
state.onSelectTokenClick
|
||||
} else {
|
||||
null
|
||||
},
|
||||
TransactionCardData(
|
||||
priceImpactWarning = priceImpactWarning,
|
||||
networkIconRes = networkIconRes,
|
||||
swapCardState = state.receiveCardData,
|
||||
modifier = Modifier.constrainAs(bottomCard) {
|
||||
top.linkTo(topCard.bottom, margin = marginCard)
|
||||
},
|
||||
onSelectTokenClick = state.onSelectTokenClick,
|
||||
)
|
||||
val marginButton = TangemTheme.dimens.spacing32
|
||||
SwapButton(
|
||||
|
|
@ -200,6 +176,48 @@ private fun MainInfo(state: SwapStateHolder) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransactionCardData(
|
||||
priceImpactWarning: SwapWarning.HighPriceImpact?,
|
||||
networkIconRes: Int?,
|
||||
swapCardState: SwapCardState,
|
||||
onSelectTokenClick: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (swapCardState) {
|
||||
is SwapCardState.Empty -> {
|
||||
TransactionCardEmpty(
|
||||
type = swapCardState.type,
|
||||
amountEquivalent = swapCardState.amountEquivalent,
|
||||
textFieldValue = swapCardState.amountTextFieldValue,
|
||||
onChangeTokenClick = if (swapCardState.canSelectAnotherToken) onSelectTokenClick else null,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
is SwapCardState.SwapCardData -> {
|
||||
TransactionCard(
|
||||
type = swapCardState.type,
|
||||
balance = if (swapCardState.isBalanceHidden) {
|
||||
STARS
|
||||
} else {
|
||||
swapCardState.balance
|
||||
},
|
||||
textFieldValue = swapCardState.amountTextFieldValue,
|
||||
amountEquivalent = swapCardState.amountEquivalent,
|
||||
tokenIconUrl = swapCardState.tokenIconUrl ?: "",
|
||||
tokenCurrency = swapCardState.tokenCurrency,
|
||||
priceImpact = priceImpactWarning,
|
||||
networkIconRes = if (swapCardState.isNotNativeToken) networkIconRes else null,
|
||||
iconPlaceholder = swapCardState.coinId?.let {
|
||||
getActiveIconResByCoinId(it)
|
||||
},
|
||||
onChangeTokenClick = if (swapCardState.canSelectAnotherToken) onSelectTokenClick else null,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) {
|
||||
|
|
@ -268,7 +286,8 @@ private fun FeeItem(feeState: FeeState, currency: String) {
|
|||
}
|
||||
}
|
||||
is FeeState.Empty -> {
|
||||
SmallInfoCard(startText = titleString, endText = "")
|
||||
// show nothing
|
||||
// SmallInfoCard(startText = titleString, endText = "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -355,7 +374,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U
|
|||
|
||||
// region preview
|
||||
|
||||
private val sendCard = SwapCardData(
|
||||
private val sendCard = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.SendCard({}) {},
|
||||
amountTextFieldValue = TextFieldValue(),
|
||||
amountEquivalent = "1 000 000",
|
||||
|
|
@ -368,7 +387,7 @@ private val sendCard = SwapCardData(
|
|||
isBalanceHidden = false,
|
||||
)
|
||||
|
||||
private val receiveCard = SwapCardData(
|
||||
private val receiveCard = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReceiveCard(),
|
||||
amountTextFieldValue = TextFieldValue(),
|
||||
amountEquivalent = "1 000 000",
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ fun TransactionCard(
|
|||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Header(balance = balance, type = type)
|
||||
Header(balance = stringResource(R.string.common_balance, balance), type = type)
|
||||
|
||||
Content(
|
||||
type = type,
|
||||
|
|
@ -106,6 +106,67 @@ fun TransactionCard(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TransactionCardEmpty(
|
||||
type: TransactionCardType,
|
||||
amountEquivalent: String?,
|
||||
textFieldValue: TextFieldValue?,
|
||||
modifier: Modifier = Modifier,
|
||||
onChangeTokenClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
elevation = TangemTheme.dimens.elevation2,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Header(
|
||||
balance = stringResource(id = R.string.swapping_token_not_available),
|
||||
type = type,
|
||||
)
|
||||
|
||||
Content(
|
||||
type = type,
|
||||
amountEquivalent = amountEquivalent,
|
||||
textFieldValue = textFieldValue,
|
||||
priceImpact = null,
|
||||
)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.align(Alignment.BottomEnd)) {
|
||||
Token(
|
||||
tokenIconUrl = "",
|
||||
tokenCurrency = "",
|
||||
iconPlaceholder = R.drawable.ic_no_token_44,
|
||||
)
|
||||
}
|
||||
|
||||
if (onChangeTokenClick != null) {
|
||||
Box(modifier = Modifier.align(Alignment.CenterEnd)) {
|
||||
ChangeTokenSelector()
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.height(TangemTheme.dimens.size116)
|
||||
.width(TangemTheme.dimens.size102)
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = false),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) { onChangeTokenClick() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(type: TransactionCardType, balance: String, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
|
|
@ -134,7 +195,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
|
|||
SpacerW16()
|
||||
if (balance.isNotBlank()) {
|
||||
Text(
|
||||
text = stringResource(R.string.common_balance, balance),
|
||||
text = balance,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.feature.swap.viewmodels
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapStateData
|
||||
|
|
@ -14,8 +14,8 @@ data class SwapProcessDataState(
|
|||
val fromCurrency: Currency? = null,
|
||||
@Deprecated("used in old swap mechanism")
|
||||
val toCurrency: Currency? = null,
|
||||
val fromCryptoCurrency: CryptoCurrency? = null,
|
||||
val toCryptoCurrency: CryptoCurrency? = null,
|
||||
val fromCryptoCurrency: CryptoCurrencyStatus? = null,
|
||||
val toCryptoCurrency: CryptoCurrencyStatus? = null,
|
||||
// Amount from input
|
||||
val amount: String? = null,
|
||||
val approveDataModel: RequestApproveStateData? = null,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import com.tangem.core.ui.utils.InputNumberFormatter
|
|||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.analytics.SwapEvents
|
||||
import com.tangem.feature.swap.domain.BlockchainInteractor
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
|
|
@ -49,11 +51,13 @@ internal class SwapViewModel @Inject constructor(
|
|||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
private val initialCryptoCurrency: CryptoCurrency = savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY]
|
||||
?: error("no expected parameter CryptoCurrency found`")
|
||||
private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
|
||||
private var isBalanceHidden = true
|
||||
|
||||
|
|
@ -88,11 +92,20 @@ internal class SwapViewModel @Inject constructor(
|
|||
get() = swapRouter.currentScreen
|
||||
|
||||
init {
|
||||
swapInteractor.initDerivationPathAndNetwork(
|
||||
derivationPath = initialCryptoCurrency.network.derivationPath.value,
|
||||
network = initialCryptoCurrency.network,
|
||||
)
|
||||
initTokens()
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
swapInteractor.getSelectedWallet()?.let {
|
||||
initialCryptoCurrencyStatus =
|
||||
requireNotNull(getCryptoCurrencyStatusUseCase(it.walletId, initialCryptoCurrency.id).getOrNull()) {
|
||||
"Failed to get initial crypto currency status"
|
||||
}
|
||||
|
||||
swapInteractor.initDerivationPathAndNetwork(
|
||||
derivationPath = initialCryptoCurrency.network.derivationPath.value,
|
||||
network = initialCryptoCurrency.network,
|
||||
)
|
||||
initTokens()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
|
|
@ -137,44 +150,38 @@ internal class SwapViewModel @Inject constructor(
|
|||
runCatching(dispatchers.io) {
|
||||
swapInteractor.getTokensDataState(initialCryptoCurrency)
|
||||
}.onSuccess { state ->
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = initialCryptoCurrency,
|
||||
toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency,
|
||||
tokensDataState = state,
|
||||
)
|
||||
|
||||
updateTokensState(state)
|
||||
startLoadingQuotes(
|
||||
fromToken = initialCryptoCurrency,
|
||||
toToken = state.toGroup.available.first().currencyStatus.currency,
|
||||
amount = lastAmount.value,
|
||||
)
|
||||
applyInitialTokenChoice(state, selectInitialCurrencyToSwap(state))
|
||||
}.onFailure {
|
||||
Timber.tag(loggingTag).e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// old flow
|
||||
// viewModelScope.launch(dispatchers.main) {
|
||||
// runCatching(dispatchers.io) {
|
||||
// swapInteractor.initTokensToSwap(currency)
|
||||
// }
|
||||
// .onSuccess { state ->
|
||||
// // dataState = dataState.copy(
|
||||
// // fromCurrency = state.preselectTokens.fromToken,
|
||||
// // toCurrency = state.preselectTokens.toToken,
|
||||
// // )
|
||||
// // updateTokensState(dataState = state.foundTokensState)
|
||||
// // startLoadingQuotes(
|
||||
// // fromToken = state.preselectTokens.fromToken,
|
||||
// // toToken = state.preselectTokens.toToken,
|
||||
// // amount = lastAmount.value,
|
||||
// // )
|
||||
// }
|
||||
// .onFailure {
|
||||
// Timber.tag(loggingTag).e(it)
|
||||
// }
|
||||
// }
|
||||
private fun applyInitialTokenChoice(state: TokensDataStateExpress, selectedCurrency: CryptoCurrencyStatus?) {
|
||||
val fromCurrencyStatus = initialCryptoCurrencyStatus
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = fromCurrencyStatus,
|
||||
toCryptoCurrency = selectedCurrency,
|
||||
tokensDataState = state,
|
||||
)
|
||||
if (selectedCurrency == null) {
|
||||
uiState = stateBuilder.createNoAvailableTokensToSwapState(
|
||||
uiStateHolder = uiState,
|
||||
fromToken = fromCurrencyStatus,
|
||||
)
|
||||
} else {
|
||||
startLoadingQuotes(
|
||||
fromToken = fromCurrencyStatus.currency,
|
||||
toToken = selectedCurrency.currency,
|
||||
amount = lastAmount.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectInitialCurrencyToSwap(state: TokensDataStateExpress): CryptoCurrencyStatus? {
|
||||
// todo add algorithm to select initial currency
|
||||
return state.toGroup.available.firstOrNull()?.currencyStatus
|
||||
}
|
||||
|
||||
private fun updateTokensState(dataState: TokensDataStateExpress) {
|
||||
|
|
@ -203,7 +210,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
val toCurrency = dataState.toCryptoCurrency
|
||||
val amount = dataState.amount
|
||||
if (fromCurrency != null && toCurrency != null && amount != null) {
|
||||
startLoadingQuotes(fromCurrency, toCurrency, amount)
|
||||
startLoadingQuotes(fromCurrency.currency, toCurrency.currency, amount)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -285,8 +292,8 @@ internal class SwapViewModel @Inject constructor(
|
|||
swapInteractor.onSwap(
|
||||
networkId = dataState.networkId,
|
||||
swapStateData = requireNotNull(dataState.swapDataModel),
|
||||
currencyToSend = requireNotNull(dataState.fromCryptoCurrency),
|
||||
currencyToGet = requireNotNull(dataState.toCryptoCurrency),
|
||||
currencyToSend = requireNotNull(dataState.fromCryptoCurrency?.currency),
|
||||
currencyToGet = requireNotNull(dataState.toCryptoCurrency?.currency),
|
||||
amountToSwap = requireNotNull(dataState.amount),
|
||||
fee = requireNotNull(dataState.selectedFee),
|
||||
)
|
||||
|
|
@ -335,10 +342,10 @@ internal class SwapViewModel @Inject constructor(
|
|||
approveData = requireNotNull(dataState.approveDataModel) {
|
||||
"dataState.approveDataModel might not be null"
|
||||
},
|
||||
forTokenContractAddress = (dataState.fromCryptoCurrency as? CryptoCurrency.Token)
|
||||
forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token)
|
||||
?.contractAddress
|
||||
?: "",
|
||||
fromToken = requireNotNull(dataState.fromCryptoCurrency) {
|
||||
fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) {
|
||||
"dataState.fromCurrency might not be null"
|
||||
},
|
||||
approveType = requireNotNull(uiState.permissionState as? SwapPermissionState.ReadyForRequest) {
|
||||
|
|
@ -384,28 +391,34 @@ internal class SwapViewModel @Inject constructor(
|
|||
private fun onTokenSelect(id: String) {
|
||||
val tokens = dataState.tokensDataState ?: return
|
||||
|
||||
val foundToken = tokens.toGroup.available.firstOrNull {
|
||||
it.currencyStatus.currency.id.value == id
|
||||
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
|
||||
}
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.currencyStatus?.currency?.symbol),
|
||||
)
|
||||
|
||||
if (foundToken != null) {
|
||||
val fromToken: CryptoCurrency
|
||||
val toToken: CryptoCurrency
|
||||
val fromToken: CryptoCurrencyStatus
|
||||
val toToken: CryptoCurrencyStatus
|
||||
if (isOrderReversed) {
|
||||
fromToken = foundToken.currencyStatus.currency
|
||||
toToken = initialCryptoCurrency
|
||||
fromToken = foundToken.currencyStatus
|
||||
toToken = initialCryptoCurrencyStatus
|
||||
} else {
|
||||
fromToken = initialCryptoCurrency
|
||||
toToken = foundToken.currencyStatus.currency
|
||||
fromToken = initialCryptoCurrencyStatus
|
||||
toToken = foundToken.currencyStatus
|
||||
}
|
||||
dataState = dataState.copy(
|
||||
fromCryptoCurrency = fromToken,
|
||||
toCryptoCurrency = toToken,
|
||||
)
|
||||
startLoadingQuotes(fromToken, toToken, lastAmount.value)
|
||||
startLoadingQuotes(fromToken.currency, toToken.currency, lastAmount.value)
|
||||
swapRouter.openScreen(SwapNavScreen.Main)
|
||||
}
|
||||
}
|
||||
|
|
@ -419,13 +432,13 @@ internal class SwapViewModel @Inject constructor(
|
|||
toCryptoCurrency = newToToken,
|
||||
)
|
||||
isOrderReversed = !isOrderReversed
|
||||
val decimals = blockchainInteractor.getTokenDecimals(newFromToken)
|
||||
val decimals = newFromToken.currency.decimals
|
||||
lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value)
|
||||
uiState = stateBuilder.updateSwapAmount(
|
||||
uiState,
|
||||
inputNumberFormatter.formatWithThousands(lastAmount.value, decimals),
|
||||
)
|
||||
startLoadingQuotes(newFromToken, newToToken, lastAmount.value)
|
||||
startLoadingQuotes(newFromToken.currency, newToToken.currency, lastAmount.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -433,20 +446,20 @@ internal class SwapViewModel @Inject constructor(
|
|||
val fromToken = dataState.fromCryptoCurrency
|
||||
val toToken = dataState.toCryptoCurrency
|
||||
if (fromToken != null && toToken != null) {
|
||||
val decimals = blockchainInteractor.getTokenDecimals(fromToken)
|
||||
val decimals = fromToken.currency.decimals
|
||||
val cutValue = cutAmountWithDecimals(decimals, value)
|
||||
lastAmount.value = cutValue
|
||||
uiState =
|
||||
stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals))
|
||||
amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) {
|
||||
startLoadingQuotes(fromToken, toToken, lastAmount.value)
|
||||
startLoadingQuotes(fromToken.currency, toToken.currency, lastAmount.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMaxAmountClicked() {
|
||||
dataState.fromCryptoCurrency?.let {
|
||||
val balance = swapInteractor.getTokenBalance(initialCryptoCurrency.network.id.value, it)
|
||||
val balance = swapInteractor.getTokenBalance(initialCryptoCurrency.network.id.value, it.currency)
|
||||
onAmountChanged(balance.formatToUIRepresentation())
|
||||
}
|
||||
}
|
||||
|
|
@ -522,7 +535,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
dataState = dataState.copy(selectedFee = feeItem.data)
|
||||
val spendAmount = dataState.amount?.let { amount ->
|
||||
val fromToken = dataState.fromCryptoCurrency ?: return@let null
|
||||
swapInteractor.getSwapAmountForToken(amount, fromToken)
|
||||
swapInteractor.getSwapAmountForToken(amount, fromToken.currency)
|
||||
} ?: dataState.approveDataModel?.fromTokenAmount
|
||||
spendAmount ?: return@UiActions
|
||||
val fromToken = dataState.fromCryptoCurrency ?: return@UiActions
|
||||
|
|
@ -531,7 +544,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
fee = feeItem.data.feeValue,
|
||||
spendAmount = spendAmount,
|
||||
networkId = dataState.networkId,
|
||||
fromToken = fromToken,
|
||||
fromToken = fromToken.currency,
|
||||
)
|
||||
uiState = stateBuilder.updateFeeSelectedItem(uiState, feeItem, isFeeEnough)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue