diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 042bf0f667..ee4f964106 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -4,6 +4,7 @@ import androidx.core.os.bundleOf import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.AmountType import com.tangem.core.analytics.Analytics +import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -155,11 +156,11 @@ class TradeCryptoMiddleware { return when (this) { is Currency.Blockchain -> { SwapCurrency.NativeToken( - id = blockchain.id, + id = blockchain.toCoinId(), name = this.currencyName, symbol = this.currencySymbol, networkId = this.blockchain.toNetworkId(), - logoUrl = getIconUrl(this.blockchain.toNetworkId()), + logoUrl = getIconUrl(this.blockchain.toCoinId()), ) } is Currency.Token -> SwapCurrency.NonNativeToken( diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index a44ccfc2de..7af01a37c6 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -9,6 +9,7 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.Currency @@ -59,6 +60,16 @@ class UserWalletManagerImpl( } } + override fun getNativeTokenForNetwork(networkId: String): Currency { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + return NativeToken( + id = blockchain.toCoinId(), + name = blockchain.fullName, + symbol = blockchain.currency, + networkId = networkId, + ) + } + override fun getWalletId(): String { return appStateHolder.getActualCard()?.let { UserWalletIdBuilder.card(it) @@ -148,11 +159,6 @@ class UserWalletManagerImpl( ) } - override fun getCurrencyByNetworkId(networkId: String): String { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - return blockchain.currency - } - private fun addNativeTokenToWalletAction(token: NativeToken, card: CardDTO): Action { val blockchain = Blockchain.fromNetworkId(token.networkId) val scanResponse = appStateHolder.scanResponse diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Blockchain.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt similarity index 51% rename from core/ui/src/main/java/com/tangem/core/ui/extensions/Blockchain.kt rename to core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 352a685b6c..ae4d3a5c93 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/Blockchain.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -34,4 +34,42 @@ fun getActiveIconRes(blockchainId: String): Int { "DASH" -> R.drawable.img_dash_22 else -> R.drawable.ic_alert_24 } +} + +@Suppress("ComplexMethod") +@DrawableRes +fun getActiveIconResByCoinId(coinId: String, networkId: String): Int { + return when (coinId) { + "binancecoin" -> R.drawable.img_bsc_22 + "bitcoin" -> R.drawable.img_btc_22 + "bitcoin-cash" -> R.drawable.img_btc_cash_22 + "ethereum" -> { + when (networkId) { + "ethereum" -> R.drawable.img_eth_22 + "arbitrum-one" -> R.drawable.img_arbitrum_22 + "optimistic-ethereum" -> R.drawable.img_optimism_22 + else -> R.drawable.ic_alert_24 + } + } + "ethereum-classic" -> R.drawable.img_eth_classic_22 + "stellar" -> R.drawable.img_stellar_22 + "cardano" -> R.drawable.img_cardano_22 + "matic-network" -> R.drawable.img_polygon_22 + "avalanche-2" -> R.drawable.img_avalanche_22 + "solana" -> R.drawable.img_solana_22 + "fantom" -> R.drawable.img_fantom_22 + "tron" -> R.drawable.img_tron_22 + "polkadot" -> R.drawable.img_polkadot_22 + "litecoin" -> R.drawable.img_litecoin_22 + "rootstock" -> R.drawable.img_rsk_22 + "tezos" -> R.drawable.img_tezos_22 + "ripple" -> R.drawable.img_xrp_22 + "dogecoin" -> R.drawable.img_dogecoin_22 + "xdai" -> R.drawable.img_gnosis_22 + "ethereum-pow-iou" -> R.drawable.img_eth_pow_22 + "ethereumfair" -> R.drawable.img_eth_fair_22 + "kusama" -> R.drawable.img_kusama_22 + "dash" -> R.drawable.img_dash_22 + else -> R.drawable.ic_alert_24 + } } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt index b40a330485..a3b46cdb29 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt @@ -31,11 +31,12 @@ class TokensConverter @Inject constructor() : Converter, @@ -242,9 +248,9 @@ internal class SwapInteractorImpl @Inject constructor( tokensInWallet.firstOrNull { it.symbol != initialToken.symbol } ?: loadedTokens.first { it.symbol != initialToken.symbol } } else { - val findUsdt = loadedTokens.firstOrNull { it.symbol == "USDT" && it.symbol != initialToken.symbol } + val findUsdt = loadedTokens.firstOrNull { it.symbol == USDT_SYMBOL && it.symbol != initialToken.symbol } if (findUsdt == null) { - val findUsdc = loadedTokens.firstOrNull { it.symbol == "USDC" && it.symbol != initialToken.symbol } + val findUsdc = loadedTokens.firstOrNull { it.symbol == USDC_SYMBOL && it.symbol != initialToken.symbol } findUsdc ?: loadedTokens.first { it.symbol != initialToken.symbol } } else { findUsdt @@ -315,8 +321,8 @@ internal class SwapInteractorImpl @Inject constructor( SwapAmount(it.value, it.decimals) } return SwapState.EmptyAmountState( - fromTokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: "", - toTokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: "", + fromTokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(), + toTokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(), ) } @@ -342,17 +348,24 @@ internal class SwapInteractorImpl @Inject constructor( val quoteDataModel = quotes.dataModel if (quoteDataModel != null) { val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken)) + val fee = transactionManager.calculateFee( + networkId = networkId, + estimatedGas = quoteDataModel.estimatedGas, + gasPrice = transactionData.gasPrice, + ) + val feeFiat = getFormattedFiatFee(networkId, fromToken.id, toToken.id, fee) + val formattedFee = amountFormatter.formatBigDecimalAmountToUI( + amount = fee, + decimals = transactionManager.getNativeTokenDecimals(networkId), + currency = userWalletManager.getNetworkCurrency(networkId), + ) + feeFiat val swapState = updateBalances( networkId = networkId, fromToken = fromToken, toToken = toToken, fromTokenAmount = quoteDataModel.fromTokenAmount, toTokenAmount = quoteDataModel.toTokenAmount, - fee = transactionManager.calculateFee( - networkId = networkId, - estimatedGas = quoteDataModel.estimatedGas, - gasPrice = transactionData.gasPrice, - ), + formattedFee = formattedFee, preparedSwapConfigState = preparedSwapConfigState, swapDataModel = null, ) @@ -362,6 +375,7 @@ internal class SwapInteractorImpl @Inject constructor( quotesLoadedState = swapState, estimatedGas = quoteDataModel.estimatedGas, transactionData = transactionData, + formattedFee = formattedFee, ) } else { return SwapState.SwapError(quotes.error) @@ -369,6 +383,20 @@ internal class SwapInteractorImpl @Inject constructor( } } + private suspend fun getFormattedFiatFee( + networkId: String, + fromTokenId: String, + toTokenId: String, + fee: BigDecimal, + ): String { + val appCurrency = userWalletManager.getUserAppCurrency() + val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) + val rates = repository.getRates(appCurrency.code, listOf(fromTokenId, toTokenId, nativeToken.id)) + return rates[nativeToken.id]?.toBigDecimal()?.let { rate -> + " (${fee.toFiatString(rate, appCurrency.symbol)})" + }.orEmpty() + } + /** * Load swap data calls only if spend is allowed for token contract address */ @@ -392,17 +420,24 @@ internal class SwapInteractorImpl @Inject constructor( ).let { val swapData = it.dataModel if (swapData != null) { + val fee = transactionManager.calculateFee( + networkId = networkId, + estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS, + gasPrice = swapData.transaction.gasPrice, + ) + val feeFiat = getFormattedFiatFee(networkId, fromToken.id, toToken.id, fee) + val formattedFee = amountFormatter.formatBigDecimalAmountToUI( + amount = fee, + decimals = transactionManager.getNativeTokenDecimals(networkId), + currency = userWalletManager.getNetworkCurrency(networkId), + ) + feeFiat val swapState = updateBalances( networkId = networkId, fromToken = fromToken, toToken = toToken, fromTokenAmount = swapData.fromTokenAmount, toTokenAmount = swapData.toTokenAmount, - fee = transactionManager.calculateFee( - networkId = networkId, - estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS, - gasPrice = swapData.transaction.gasPrice, - ), + formattedFee = formattedFee, preparedSwapConfigState = preparedSwapConfigState, swapDataModel = swapData, ) @@ -422,12 +457,13 @@ internal class SwapInteractorImpl @Inject constructor( toToken: Currency, fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, - fee: BigDecimal, + formattedFee: String, preparedSwapConfigState: PreparedSwapConfigState, swapDataModel: SwapDataModel?, ): SwapState.QuotesLoadedState { val appCurrency = userWalletManager.getUserAppCurrency() - val rates = repository.getRates(appCurrency.code, listOf(fromToken.id, toToken.id)) + val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) + val rates = repository.getRates(appCurrency.code, listOf(fromToken.id, toToken.id, nativeToken.id)) val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId) val fromTokenBalance = tokensBalance[fromToken.symbol]?.let { amountFormatter.formatProxyAmountToUI(it, "") @@ -438,7 +474,7 @@ internal class SwapInteractorImpl @Inject constructor( return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( tokenAmount = fromTokenAmount, - tokenId = fromToken.id, + coinId = fromToken.id, tokenWalletBalance = fromTokenBalance ?: ZERO_BALANCE, tokenFiatBalance = fromTokenAmount.value.toFiatString( rates[fromToken.id]?.toBigDecimal() ?: BigDecimal.ZERO, @@ -447,18 +483,14 @@ internal class SwapInteractorImpl @Inject constructor( ), toTokenInfo = TokenSwapInfo( tokenAmount = toTokenAmount, - tokenId = toToken.id, + coinId = toToken.id, tokenWalletBalance = toTokenBalance ?: ZERO_BALANCE, tokenFiatBalance = toTokenAmount.value.toFiatString( rates[toToken.id]?.toBigDecimal() ?: BigDecimal.ZERO, appCurrency.symbol, ), ), - fee = amountFormatter.formatBigDecimalAmountToUI( - amount = fee, - decimals = transactionManager.getNativeTokenDecimals(networkId), - currency = userWalletManager.getCurrencyByNetworkId(networkId), - ), + fee = formattedFee, networkCurrency = userWalletManager.getNetworkCurrency(networkId), preparedSwapConfigState = preparedSwapConfigState, swapDataModel = swapDataModel, @@ -471,6 +503,7 @@ internal class SwapInteractorImpl @Inject constructor( quotesLoadedState: SwapState.QuotesLoadedState, estimatedGas: Int, transactionData: ApproveModel, + formattedFee: String, ): SwapState.QuotesLoadedState { if (allowPermissionsHandler.isAddressAllowanceInProgress(getTokenAddress(fromToken))) { return quotesLoadedState.copy( @@ -479,15 +512,11 @@ internal class SwapInteractorImpl @Inject constructor( } return quotesLoadedState.copy( permissionState = PermissionDataState.PermissionReadyForRequest( - currency = userWalletManager.getCurrencyByNetworkId(networkId), - amount = "∞", + currency = fromToken.symbol, + amount = INFINITY_SYMBOL, walletAddress = getWalletAddress(networkId), spenderAddress = transactionData.toAddress, - fee = transactionManager.calculateFee( - networkId = networkId, - gasPrice = transactionData.gasPrice, - estimatedGas = estimatedGas, - ).toPlainString(), + fee = formattedFee, requestApproveData = RequestApproveStateData( estimatedGas = estimatedGas, approveModel = transactionData, @@ -497,10 +526,14 @@ internal class SwapInteractorImpl @Inject constructor( } private fun isBalanceEnough(fromToken: Currency, networkId: String, amount: SwapAmount, fee: BigDecimal?): Boolean { - /** to compare [BigDecimal] use only comparator */ - val balance = userWalletManager.getCurrentWalletTokensBalance(networkId)[fromToken.symbol]?.value - ?: BigDecimal.ZERO - return balance > amount.value.plus(fee ?: BigDecimal.ZERO) + return if (fromToken is Currency.NonNativeToken) { + (userWalletManager.getCurrentWalletTokensBalance(networkId)[fromToken.symbol]?.value + ?: BigDecimal.ZERO) >= amount.value + } else { + /** to compare [BigDecimal] use only comparator */ + (userWalletManager.getCurrentWalletTokensBalance(networkId)[fromToken.symbol]?.value + ?: BigDecimal.ZERO) > amount.value.plus(fee ?: BigDecimal.ZERO) + } } private fun getWalletAddress(networkId: String): String { @@ -544,5 +577,8 @@ internal class SwapInteractorImpl @Inject constructor( private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" private const val TWENTY_FIVE_PERCENTS = 25 private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.5 + private const val USDT_SYMBOL = "USDT" + private const val USDC_SYMBOL = "USDC" + private const val INFINITY_SYMBOL = "∞" } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 3b7f728f9c..688ad65b7b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -46,7 +46,7 @@ sealed class PermissionDataState { data class TokenSwapInfo( val tokenAmount: SwapAmount, - val tokenId: String, + val coinId: String, val tokenWalletBalance: String, val tokenFiatBalance: String, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 139a7c50de..a6ac559be4 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -6,6 +6,7 @@ data class SwapStateHolder( val sendCardData: SwapCardData, val receiveCardData: SwapCardData, val networkCurrency: String, + val networkId: String, val fee: FeeState = FeeState.Empty, val warnings: List = emptyList(), val alert: SwapWarning.GenericWarning? = null, @@ -29,8 +30,8 @@ data class SwapCardData( val type: TransactionCardType, val amount: String?, val amountEquivalent: String?, + val coinId: String?, val tokenIconUrl: String, - val tokenId: String, val tokenCurrency: String, val balance: String, @DrawableRes val networkIconRes: Int? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 60f8a74218..fdc4f6ff17 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -8,4 +8,5 @@ data class UiActions( val onGivePermissionClick: () -> Unit, val onChangeCardsClicked: () -> Unit, val onBackClicked: () -> Unit, + val onMaxAmountSelected: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index a4a4f38520..a3dc6ade38 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -29,13 +29,14 @@ class StateBuilder(val actions: UiActions) { fun createInitialLoadingState(initialCurrency: Currency): SwapStateHolder { return SwapStateHolder( + networkId = initialCurrency.networkId, sendCardData = SwapCardData( type = TransactionCardType.SendCard(actions.onAmountChanged), amount = null, amountEquivalent = null, tokenIconUrl = initialCurrency.logoUrl, tokenCurrency = initialCurrency.symbol, - tokenId = initialCurrency.id, + coinId = initialCurrency.id, canSelectAnotherToken = false, balance = "", ), @@ -47,7 +48,7 @@ class StateBuilder(val actions: UiActions) { tokenCurrency = "", canSelectAnotherToken = false, balance = "", - tokenId = "", + coinId = null, ), fee = FeeState.Loading, networkCurrency = initialCurrency.symbol, @@ -55,6 +56,8 @@ class StateBuilder(val actions: UiActions) { onRefresh = {}, onBackClicked = actions.onBackClicked, onChangeCardsClicked = actions.onChangeCardsClicked, + onMaxAmountSelected = actions.onMaxAmountSelected, + updateInProgress = true, ) } @@ -68,10 +71,10 @@ class StateBuilder(val actions: UiActions) { sendCardData = SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amount = uiStateHolder.sendCardData.amount, - amountEquivalent = uiStateHolder.sendCardData.amountEquivalent, + amountEquivalent = null, tokenIconUrl = fromToken.logoUrl, tokenCurrency = fromToken.symbol, - tokenId = fromToken.id, + coinId = fromToken.id, canSelectAnotherToken = mainTokenId != fromToken.id, balance = "", ), @@ -81,13 +84,14 @@ class StateBuilder(val actions: UiActions) { amountEquivalent = null, tokenIconUrl = toToken.logoUrl, tokenCurrency = toToken.symbol, - tokenId = toToken.id, + coinId = toToken.id, canSelectAnotherToken = mainTokenId != toToken.id, balance = "", ), fee = FeeState.Loading, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), permissionState = uiStateHolder.permissionState, + updateInProgress = true, ) } @@ -111,10 +115,10 @@ class StateBuilder(val actions: UiActions) { return uiStateHolder.copy( sendCardData = SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), - amount = quoteModel.fromTokenInfo.tokenAmount.formatToUIRepresentation(), + amount = uiStateHolder.sendCardData.amount, amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance, tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - tokenId = quoteModel.fromTokenInfo.tokenId, + coinId = quoteModel.fromTokenInfo.coinId, tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = quoteModel.fromTokenInfo.tokenWalletBalance, @@ -124,7 +128,7 @@ class StateBuilder(val actions: UiActions) { amount = quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation(), amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, - tokenId = quoteModel.toTokenInfo.tokenId, + coinId = quoteModel.toTokenInfo.coinId, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, balance = quoteModel.toTokenInfo.tokenWalletBalance, @@ -140,6 +144,7 @@ class StateBuilder(val actions: UiActions) { loading = false, onClick = actions.onSwapClick, ), + updateInProgress = false, ) } @@ -151,9 +156,9 @@ class StateBuilder(val actions: UiActions) { sendCardData = SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amount = uiStateHolder.sendCardData.amount, - amountEquivalent = uiStateHolder.sendCardData.amountEquivalent, + amountEquivalent = "", tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - tokenId = uiStateHolder.sendCardData.tokenId, + coinId = uiStateHolder.sendCardData.coinId, tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = emptyAmountState.fromTokenWalletBalance, @@ -161,9 +166,9 @@ class StateBuilder(val actions: UiActions) { receiveCardData = SwapCardData( type = TransactionCardType.ReceiveCard(), amount = "0", - amountEquivalent = uiStateHolder.receiveCardData.amountEquivalent, + amountEquivalent = "", tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, - tokenId = uiStateHolder.receiveCardData.tokenId, + coinId = uiStateHolder.receiveCardData.coinId, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, balance = emptyAmountState.toTokenWalletBalance, @@ -174,6 +179,7 @@ class StateBuilder(val actions: UiActions) { loading = false, onClick = { }, ), + updateInProgress = false, ) } @@ -251,6 +257,7 @@ class StateBuilder(val actions: UiActions) { message = null, onClick = onAlertClick, ), + updateInProgress = false, ) } @@ -302,4 +309,10 @@ class StateBuilder(val actions: UiActions) { const val ADDRESS_FIRST_PART_LENGTH = 7 const val ADDRESS_SECOND_PART_LENGTH = 4 } + + fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder { + return uiState.copy( + updateInProgress = true, + ) + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 055d2f7db8..2f7078e144 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import com.tangem.core.ui.res.TangemTheme @@ -34,6 +35,10 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { ) } else { // Required "else" block to prevent compose crash + // always close BS if its empty, cause user should not see this + LaunchedEffect(Unit) { + coroutineScope.launch { bottomSheetState.hide() } + } Box(modifier = Modifier.fillMaxSize()) } }, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 4a8627a760..4fda317c65 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -39,7 +39,7 @@ import com.tangem.core.ui.components.SmallInfoCardWithWarning import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.getActiveIconResByCoinId import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.FeeState import com.tangem.feature.swap.models.SwapButton @@ -137,7 +137,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick: modifier = Modifier .fillMaxWidth() .background(TangemTheme.colors.button.secondary) - .clickable { state.onMaxAmountSelected } + .clickable { state.onMaxAmountSelected?.invoke() } .padding( horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing16, @@ -170,7 +170,9 @@ private fun MainInfo(state: SwapStateHolder) { tokenIconUrl = state.sendCardData.tokenIconUrl, tokenCurrency = state.sendCardData.tokenCurrency, networkIconRes = state.sendCardData.networkIconRes, - iconPlaceholder = getActiveIconRes(state.sendCardData.tokenId), + iconPlaceholder = state.sendCardData.coinId?.let { + getActiveIconResByCoinId(it, state.networkId) + }, onChangeTokenClick = if (state.sendCardData.canSelectAnotherToken) state.onSelectTokenClick else null, ) SpacerH16() @@ -182,7 +184,9 @@ private fun MainInfo(state: SwapStateHolder) { tokenIconUrl = state.receiveCardData.tokenIconUrl, tokenCurrency = state.receiveCardData.tokenCurrency, networkIconRes = state.receiveCardData.networkIconRes, - iconPlaceholder = getActiveIconRes(state.receiveCardData.tokenId), + iconPlaceholder = state.receiveCardData.coinId?.let { + getActiveIconResByCoinId(it, state.networkId) + }, onChangeTokenClick = if (state.receiveCardData.canSelectAnotherToken) { state.onSelectTokenClick } else { @@ -190,13 +194,13 @@ private fun MainInfo(state: SwapStateHolder) { }, ) } - SwapButton() + SwapButton(state) } } @OptIn(ExperimentalMaterialApi::class) @Composable -private fun SwapButton() { +private fun SwapButton(state: SwapStateHolder) { Card( elevation = TangemTheme.dimens.elevation3, shape = CircleShape, @@ -313,7 +317,7 @@ private val sendCard = SwapCardData( networkIconRes = R.drawable.img_polygon_22, canSelectAnotherToken = false, balance = "123", - tokenId = "", + coinId = "", ) private val receiveCard = SwapCardData( @@ -325,10 +329,11 @@ private val receiveCard = SwapCardData( networkIconRes = R.drawable.img_polygon_22, canSelectAnotherToken = true, balance = "33333", - tokenId = "", + coinId = "", ) private val state = SwapStateHolder( + networkId = "ethereum", sendCardData = sendCard, receiveCardData = receiveCard, fee = FeeState.Loaded(fee = "0.155 MATIC (0.14 $)"), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index c62ebacd9c..cd753171ee 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -351,13 +351,14 @@ fun Token( .align(Alignment.BottomStart) .size(TangemTheme.dimens.size36) + val data = tokenIconUrl.ifEmpty { + iconPlaceholder + } SubcomposeAsyncImage( modifier = tokenImageModifier, model = ImageRequest.Builder(LocalContext.current) - .data(tokenIconUrl) + .data(data) .crossfade(true) - .error(iconPlaceholder ?: 0) - .placeholder(iconPlaceholder ?: 0) .build(), loading = { TokenImageShimmer(modifier = tokenImageModifier) }, // error = { CurrencyPlaceholderIcon(modifier = tokenImageModifier, tokenCurrency) }, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index d4ceed6688..99a41ea8b1 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.FoundTokensState import com.tangem.feature.swap.domain.models.ui.PermissionDataState import com.tangem.feature.swap.domain.models.ui.SwapState @@ -23,6 +24,7 @@ import com.tangem.feature.swap.ui.StateBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.toFormattedString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import kotlinx.serialization.decodeFromString @@ -125,6 +127,7 @@ internal class SwapViewModel @Inject constructor( return PeriodicTask( UPDATE_DELAY, task = { + uiState = stateBuilder.createSilentLoadState(uiState) runCatching(dispatchers.io) { dataState = dataState.copy( amount = amount, @@ -244,7 +247,7 @@ internal class SwapViewModel @Inject constructor( private fun onSearchEntered(searchQuery: String) { viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { - swapInteractor.onSearchToken(dataState.networkId, searchQuery) + swapInteractor.searchTokens(dataState.networkId, searchQuery) } .onSuccess { updateTokensState(it) @@ -283,22 +286,36 @@ internal class SwapViewModel @Inject constructor( toCurrency = newToToken, ) isOrderReversed = !isOrderReversed + lastAmount.value = cutAmountWithDecimals(swapInteractor.getTokenDecimals(newFromToken), lastAmount.value) + uiState = stateBuilder.updateSwapAmount(uiState, lastAmount.value) startLoadingQuotes(newFromToken, newToToken, lastAmount.value) } } private fun onAmountChanged(value: String) { - uiState = stateBuilder.updateSwapAmount(uiState, value) - lastAmount.value = value - amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) { - val fromToken = dataState.fromCurrency - val toToken = dataState.toCurrency - if (fromToken != null && toToken != null) { + val fromToken = dataState.fromCurrency + val toToken = dataState.toCurrency + if (fromToken != null && toToken != null) { + val cutValue = cutAmountWithDecimals(swapInteractor.getTokenDecimals(fromToken), value) + uiState = stateBuilder.updateSwapAmount(uiState, cutValue) + lastAmount.value = cutValue + amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) { startLoadingQuotes(fromToken, toToken, lastAmount.value) } } } + private fun onMaxAmountClicked() { + dataState.fromCurrency?.let { + val balance = swapInteractor.getTokenBalance(it) + onAmountChanged(balance.formatToUIRepresentation()) + } + } + + private fun cutAmountWithDecimals(maxDecimals: Int, amount: String): String { + return amount.toBigDecimalOrNull()?.toFormattedString(maxDecimals) ?: INITIAL_AMOUNT + } + private fun createUiActions(): UiActions { return UiActions( onSearchEntered = { onSearchEntered(it) }, @@ -308,12 +325,13 @@ internal class SwapViewModel @Inject constructor( onGivePermissionClick = { givePermissionsToSwap() }, onChangeCardsClicked = { onChangeCardsClicked() }, onBackClicked = { onSearchEntered("") }, + onMaxAmountSelected = { onMaxAmountClicked() }, ) } companion object { private const val INITIAL_AMOUNT = "" - private const val UPDATE_DELAY = 60000L + private const val UPDATE_DELAY = 10000L private const val DEBOUNCE_AMOUNT_DELAY = 1000L } } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index 4fb0a3b584..607dc45eeb 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -14,6 +14,8 @@ interface UserWalletManager { */ suspend fun getUserTokens(networkId: String): List + fun getNativeTokenForNetwork(networkId: String): Currency + /** * Returns user walletId */ @@ -51,16 +53,14 @@ interface UserWalletManager { fun getNativeTokenBalance(networkId: String): ProxyAmount? + /** + * @param networkId + * @return currency name + */ fun getNetworkCurrency(networkId: String): String /** * Returns selected app currency */ fun getUserAppCurrency(): ProxyFiatCurrency - - /** - * @param networkId - * @return currency name - */ - fun getCurrencyByNetworkId(networkId: String): String } \ No newline at end of file