diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index b05033931a..33823b5d36 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -224,20 +224,18 @@ class TransactionManagerImpl( // for not EVM blockchains set gasLimit ZERO for now when (fee.data) { is TransactionFee.Single -> { - val fee = (fee.data as TransactionFee.Single).normal + val normalFee = (fee.data as TransactionFee.Single).normal val singleFee = ProxyFee( gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = fee.amount), + fee = convertToProxyAmount(amount = normalFee.amount), ) - ProxyFees( - minFee = singleFee, - normalFee = singleFee, - priorityFee = singleFee, + ProxyFees.SingleFee( + singleFee = singleFee, ) } is TransactionFee.Choosable -> { val choosableFee = fee.data as TransactionFee.Choosable - ProxyFees( + ProxyFees.MultipleFees( minFee = ProxyFee( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), @@ -280,7 +278,7 @@ class TransactionManagerImpl( ).increaseBigIntegerByPercents(increaseBy) return when (val gasPrice = walletManager.getGasPrice()) { is Result.Success -> { - createProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain) + createMultipleProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain) } is Result.Failure -> { error(gasPrice.error.message ?: gasPrice.error.customMessage) @@ -316,7 +314,7 @@ class TransactionManagerImpl( fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) - ProxyFees( + ProxyFees.MultipleFees( minFee = minProxyFee, normalFee = normalProxyFee, priorityFee = priorityProxyFee, @@ -456,7 +454,7 @@ class TransactionManagerImpl( * @param gasLimit * @param blockchain */ - private fun createProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees { + private fun createMultipleProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees { val gasPriceNormal = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE) val gasPricePriority = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE) val feeMin = gasLimit.multiply(gasPrice).toBigDecimal( @@ -495,7 +493,7 @@ class TransactionManagerImpl( decimals = blockchain.decimals(), ), ) - return ProxyFees( + return ProxyFees.MultipleFees( minFee = minFee, normalFee = normalFee, priorityFee = priorityFee, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 2ecdbabbff..b56262221a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -8,7 +8,6 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Query -import java.math.BigDecimal /** * Interface of Tangem Express API (new swap mechanism) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt index 5bdaa8f68b..86817c690a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt @@ -19,7 +19,7 @@ import com.tangem.core.ui.res.TangemTheme * https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=Ygv5sohTTHYAQcBS-4 */ @Composable -fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier) { +fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier, isClickable: Boolean = true) { Box( modifier = modifier .background(color = TangemTheme.colors.background.action) @@ -44,14 +44,16 @@ fun SimpleActionRow(title: String, description: String, modifier: Modifier = Mod ) } - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_24), - contentDescription = null, - modifier = Modifier - .align(alignment = Alignment.CenterEnd) - .padding(end = TangemTheme.dimens.spacing12), - tint = TangemTheme.colors.icon.informative, - ) + if (isClickable) { + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + modifier = Modifier + .align(alignment = Alignment.CenterEnd) + .padding(end = TangemTheme.dimens.spacing12), + tint = TangemTheme.colors.icon.informative, + ) + } } } diff --git a/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt b/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt index 1e266be497..a1995e1712 100644 --- a/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt +++ b/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt @@ -25,7 +25,7 @@ fun BigDecimal.toFormattedString( @Suppress("MagicNumber") fun BigDecimal.toFormattedCurrencyString( decimals: Int, - currency: String, + currency: String? = null, roundingMode: RoundingMode = RoundingMode.DOWN, limitNumberOfDecimals: Boolean = true, ): String { @@ -38,7 +38,8 @@ fun BigDecimal.toFormattedCurrencyString( decimals = decimalsForRounding, roundingMode = roundingMode, ) - return "$formattedAmount $currency" + val formattedCurrency = currency?.let { " $it " } ?: "" + return "$formattedAmount$formattedCurrency" } fun BigDecimal.toFiatString( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index a4bcb1fa99..b5d8b00f73 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -13,8 +13,6 @@ import com.tangem.datasource.api.express.models.response.SwapPair import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders import com.tangem.datasource.api.oneinch.OneInchApi import com.tangem.datasource.api.oneinch.OneInchApiFactory -import com.tangem.datasource.api.oneinch.OneInchErrorsHandler -import com.tangem.datasource.api.oneinch.errors.OneIncResponseException import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.extensions.fromNetworkId @@ -41,7 +39,6 @@ internal class SwapRepositoryImpl @Inject constructor( private val tangemTechApi: TangemTechApi, private val tangemExpressApi: TangemExpressApi, private val oneInchApiFactory: OneInchApiFactory, - private val oneInchErrorsHandler: OneInchErrorsHandler, private val coroutineDispatcher: CoroutineDispatcherProvider, private val configManager: ConfigManager, private val walletManagersFacade: WalletManagersFacade, @@ -203,7 +200,7 @@ internal class SwapRepositoryImpl @Inject constructor( toAddress = toAddress, ).getOrThrow() AggregatedSwapDataModel( - dataModel = expressDataConverter.convert(response) + dataModel = expressDataConverter.convert(response), ) } catch (ex: Exception) { AggregatedSwapDataModel(null, mapErrors(ex.message)) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt index 0c4278683f..6746243002 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt @@ -24,7 +24,7 @@ class ExpressDataConverter : Converter { txId = transactionDto.txId, txTo = transactionDto.txTo, txFrom = requireNotNull(transactionDto.txFrom), - txData = requireNotNull(transactionDto.txData) + txData = requireNotNull(transactionDto.txData), ) } else { ExpressTransactionModel.CEX( @@ -37,5 +37,4 @@ class ExpressDataConverter : Converter { ) } } - } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index a70de3e545..261dcbb069 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -2,7 +2,6 @@ package com.tangem.feature.swap.di import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.oneinch.OneInchApiFactory -import com.tangem.datasource.api.oneinch.OneInchErrorsHandler import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager import com.tangem.domain.walletmanager.WalletManagersFacade @@ -26,7 +25,6 @@ class SwapDataModule { tangemTechApi: TangemTechApi, tangemExpressApi: TangemExpressApi, oneInchApiFactory: OneInchApiFactory, - oneInchErrorsHandler: OneInchErrorsHandler, coroutineDispatcher: CoroutineDispatcherProvider, configManager: ConfigManager, walletManagerFacade: WalletManagersFacade, @@ -36,7 +34,6 @@ class SwapDataModule { tangemTechApi = tangemTechApi, tangemExpressApi = tangemExpressApi, oneInchApiFactory = oneInchApiFactory, - oneInchErrorsHandler = oneInchErrorsHandler, coroutineDispatcher = coroutineDispatcher, configManager = configManager, walletManagersFacade = walletManagerFacade, diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index b757775fd0..00bcf936c1 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -23,6 +23,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.transaction) + implementation(projects.domain.legacy) /** Core modules */ implementation(projects.core.utils) @@ -35,4 +37,5 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) implementation(deps.timber) + implementation(deps.tangem.blockchain) } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 8664f2f1ba..1057373c25 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -7,7 +7,6 @@ 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.* -import java.math.BigDecimal interface SwapInteractor { @@ -71,13 +70,21 @@ interface SwapInteractor { suspend fun onSwap( exchangeProviderType: ExchangeProviderType, networkId: String, - swapStateData: SwapStateData, + swapData: SwapDataModel, currencyToSend: CryptoCurrency, currencyToGet: CryptoCurrency, amountToSwap: String, fee: TxFee, ): TxState + suspend fun updateQuotesStateWithSelectedFee( + state: SwapState.QuotesLoadedState, + selectedFee: FeeType, + fromToken: CryptoCurrencyStatus, + amountToSwap: String, + networkId: String, + ): SwapState.QuotesLoadedState + /** * Returns token in wallet balance * @@ -88,14 +95,5 @@ interface SwapInteractor { fun isAvailableToSwap(networkId: String): Boolean - fun getSwapAmountForToken(amount: String, token: CryptoCurrency): SwapAmount - - suspend fun checkFeeIsEnough( - fee: BigDecimal?, - spendAmount: SwapAmount, - networkId: String, - fromToken: CryptoCurrency, - ): Boolean - fun getSelectedWallet(): UserWallet? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 0f5b79b69e..46ace65b78 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,6 +1,8 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency @@ -10,6 +12,8 @@ import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.cache.SwapDataCache @@ -24,10 +28,10 @@ import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.toFiatString -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -45,7 +49,9 @@ internal class SwapInteractorImpl @Inject constructor( private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val walletManagersFacade: WalletManagersFacade, private val quotesRepository: QuotesRepository, + private val dispatcher: CoroutineDispatcherProvider, ) : SwapInteractor { // TODO: Move to DI @@ -53,6 +59,10 @@ internal class SwapInteractorImpl @Inject constructor( AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository) } + private val getFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { + GetFeeUseCase(walletManagersFacade, dispatcher) + } + private val swapCurrencyConverter = SwapCurrencyConverter() private val amountFormatter = AmountFormatter() private var derivationPath: String? = null @@ -236,18 +246,11 @@ internal class SwapInteractorImpl @Inject constructor( createEmptyAmountState( networkId, fromToken.currency, - toToken.currency + toToken.currency, ) } } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) - val fromTokenAddress = getTokenAddress(fromToken.currency) - val toTokenAddress = getTokenAddress(toToken.currency) - val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount) - if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { - allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) - transactionManager.updateWalletManager(networkId, derivationPath) - } val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null) when (provider.type) { @@ -256,12 +259,9 @@ internal class SwapInteractorImpl @Inject constructor( networkId = networkId, fromToken = fromToken, toToken = toToken, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, provider = provider, selectedFee = selectedFee, amount = amount, - isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, ) } @@ -272,8 +272,8 @@ internal class SwapInteractorImpl @Inject constructor( toToken = toToken, provider = provider, amount = amount, - isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + selectedFee = selectedFee, ) } } @@ -284,20 +284,21 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, - fromTokenAddress: String, - toTokenAddress: String, provider: SwapProvider, selectedFee: FeeType, amount: SwapAmount, - isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, ): Pair { + val fromTokenAddress = getTokenAddress(fromToken.currency) + val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount) + if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { + allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) + transactionManager.updateWalletManager(networkId, derivationPath) + } return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { provider to loadSwapData( provider = provider, networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, fromToken = fromToken, toToken = toToken, amount = amount, @@ -313,6 +314,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, provider = provider, + selectedFee = selectedFee, ) } } @@ -323,8 +325,8 @@ internal class SwapInteractorImpl @Inject constructor( toToken: CryptoCurrencyStatus, provider: SwapProvider, amount: SwapAmount, - isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, + selectedFee: FeeType, ): Pair { return provider to loadQuoteData( exchangeProviderType = ExchangeProviderType.CEX, @@ -332,9 +334,10 @@ internal class SwapInteractorImpl @Inject constructor( amount = amount, fromTokenStatus = fromToken, toTokenStatus = toToken, - isAllowedToSpend = isAllowedToSpend, + isAllowedToSpend = true, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, provider = provider, + selectedFee = selectedFee, ) } @@ -342,7 +345,7 @@ internal class SwapInteractorImpl @Inject constructor( override suspend fun onSwap( exchangeProviderType: ExchangeProviderType, networkId: String, - swapStateData: SwapStateData, + swapData: SwapDataModel, currencyToSend: CryptoCurrency, currencyToGet: CryptoCurrency, amountToSwap: String, @@ -355,7 +358,7 @@ internal class SwapInteractorImpl @Inject constructor( ExchangeProviderType.DEX -> { onSwapDex( networkId = networkId, - swapStateData = swapStateData, + swapData = swapData, currencyToSend = currencyToSend, currencyToGet = currencyToGet, amountToSwap = amountToSwap, @@ -365,9 +368,39 @@ internal class SwapInteractorImpl @Inject constructor( } } + override suspend fun updateQuotesStateWithSelectedFee( + state: SwapState.QuotesLoadedState, + selectedFee: FeeType, + fromToken: CryptoCurrencyStatus, + amountToSwap: String, + networkId: String, + ): SwapState.QuotesLoadedState { + val amountDecimal = toBigDecimalOrNull(amountToSwap) + if (amountDecimal == null || amountDecimal.signum() == 0) { + return state + } + val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) + val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = state.txFee) + val isBalanceIncludeFeeEnough = + isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority) + val isFeeEnough = checkFeeIsEnough( + fee = feeByPriority, + spendAmount = amount, + networkId = networkId, + fromToken = fromToken.currency, + ) + return state.copy( + permissionState = PermissionDataState.Empty, + preparedSwapConfigState = state.preparedSwapConfigState.copy( + isBalanceEnough = isBalanceIncludeFeeEnough, + isFeeEnough = isFeeEnough, + ), + ) + } + private suspend fun onSwapDex( networkId: String, - swapStateData: SwapStateData, + swapData: SwapDataModel, currencyToSend: CryptoCurrency, currencyToGet: CryptoCurrency, amountToSwap: String, @@ -382,8 +415,8 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend = swapCurrencyConverter.convert(currencyToSend), feeAmount = fee.feeValue, gasLimit = fee.gasLimit, - destinationAddress = swapStateData.swapModel.transaction.txTo, - dataToSign = (swapStateData.swapModel.transaction as ExpressTransactionModel.DEX).txData, + destinationAddress = swapData.transaction.txTo, + dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData, ), isSwap = true, derivationPath = derivationPath, @@ -405,7 +438,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend.symbol, ), toAmount = amountFormatter.formatSwapAmountToUI( - swapStateData.swapModel.toTokenAmount, + swapData.toTokenAmount, currencyToGet.symbol, ), txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath) ?: "", @@ -437,12 +470,6 @@ internal class SwapInteractorImpl @Inject constructor( return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId) } - @Deprecated("used in old swap mechanism") - override fun getSwapAmountForToken(amount: String, token: CryptoCurrency): SwapAmount { - val amountDecimal = requireNotNull(toBigDecimalOrNull(amount)) { "wrong amount format" } - return SwapAmount(amountDecimal, getTokenDecimals(token)) - } - @Deprecated("used in old swap mechanism") private suspend fun onSuccessLegacyFlow(currency: CryptoCurrency) { userWalletManager.addToken(swapCurrencyConverter.convert(currency), derivationPath) @@ -526,6 +553,7 @@ internal class SwapInteractorImpl @Inject constructor( provider: SwapProvider, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, + selectedFee: FeeType, ): SwapState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency @@ -541,7 +569,7 @@ internal class SwapInteractorImpl @Inject constructor( rateType = RateType.FLOAT, ) - getState( + getQuotesState( exchangeProviderType = exchangeProviderType, quoteDataModel = quotes, amount = amount, @@ -550,11 +578,13 @@ internal class SwapInteractorImpl @Inject constructor( networkId = networkId, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + providerType = provider.type, + selectedFee = selectedFee, ) } } - private suspend fun getState( + private suspend fun getQuotesState( exchangeProviderType: ExchangeProviderType, quoteDataModel: AggregatedSwapDataModel, amount: SwapAmount, @@ -563,38 +593,59 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, + providerType: ExchangeProviderType, + selectedFee: FeeType, ): SwapState { val quoteModel = quoteDataModel.dataModel if (quoteModel != null) { + val txFee = if (providerType == ExchangeProviderType.CEX) { + getFeeForCex(amount, fromToken, networkId) + } else { + TxFeeState.Empty + } val swapState = updateBalances( networkId = networkId, fromTokenStatus = fromToken, toTokenStatus = toToken, fromTokenAmount = amount, toTokenAmount = quoteModel.toTokenAmount, - swapStateData = null, + swapData = null, + txFeeState = txFee, ) - val quotesState = when (exchangeProviderType) { + return when (exchangeProviderType) { ExchangeProviderType.DEX -> { - updatePermissionState( + val state = updatePermissionState( networkId = networkId, fromToken = fromToken.currency, swapAmount = amount, - quotesLoadedState = swapState + quotesLoadedState = swapState, + ) + state.copy( + preparedSwapConfigState = state.preparedSwapConfigState.copy( + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + ), ) } ExchangeProviderType.CEX -> { - swapState.copy(permissionState = PermissionDataState.Empty) + val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFee) + val isFeeEnough = checkFeeIsEnough( + fee = feeByPriority, + spendAmount = amount, + networkId = networkId, + fromToken = fromToken.currency, + ) + swapState.copy( + permissionState = PermissionDataState.Empty, + preparedSwapConfigState = PreparedSwapConfigState( + isFeeEnough = isFeeEnough, + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + ), + ) } } - - return quotesState.copy( - preparedSwapConfigState = quotesState.preparedSwapConfigState.copy( - isAllowedToSpend = isAllowedToSpend, - isBalanceEnough = isBalanceWithoutFeeEnough, - ), - ) } else { return SwapState.SwapError(quoteDataModel.error) } @@ -606,7 +657,7 @@ internal class SwapInteractorImpl @Inject constructor( val rates = getQuotes(nativeToken.id) return rates[nativeToken.id]?.fiatRate?.let { rate -> fees.map { fee -> - " (${fee.toFiatString(rate, appCurrency.symbol, true)})" + fee.toFiatString(rate, appCurrency.symbol, true) } }.orEmpty() } @@ -618,8 +669,6 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun loadSwapData( provider: SwapProvider, networkId: String, - fromTokenAddress: String, - toTokenAddress: String, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, amount: SwapAmount, @@ -647,11 +696,11 @@ internal class SwapInteractorImpl @Inject constructor( data = (swapData.transaction as ExpressTransactionModel.DEX).txData, derivationPath = derivationPath, ) - val txFeeState = proxyFeesToFeeState(networkId, feeData) - val feeByPriority = when (selectedFee) { - FeeType.NORMAL -> txFeeState.normalFee.feeValue - FeeType.PRIORITY -> txFeeState.priorityFee.feeValue + val txFeeState = when (feeData) { + is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId) + is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId) } + val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState) val isBalanceIncludeFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority) val isFeeEnough = checkFeeIsEnough( @@ -666,10 +715,8 @@ internal class SwapInteractorImpl @Inject constructor( toTokenStatus = toToken, fromTokenAmount = amount, toTokenAmount = swapData.toTokenAmount, - swapStateData = SwapStateData( - fee = txFeeState, - swapModel = swapData, - ), + swapData = swapData, + txFeeState = txFeeState, ) return swapState.copy( permissionState = PermissionDataState.Empty, @@ -692,7 +739,8 @@ internal class SwapInteractorImpl @Inject constructor( toTokenStatus: CryptoCurrencyStatus, fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, - swapStateData: SwapStateData?, + swapData: SwapDataModel?, + txFeeState: TxFeeState, ): SwapState.QuotesLoadedState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency @@ -719,11 +767,31 @@ internal class SwapInteractorImpl @Inject constructor( toRate = rates[toToken.id]?.fiatRate?.toDouble() ?: 0.0, ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), - swapDataModel = swapStateData, + swapDataModel = swapData, tangemFee = getTangemFee(), + txFee = txFeeState, ) } + private suspend fun getFeeForCex( + amount: SwapAmount, + fromToken: CryptoCurrencyStatus, + networkId: String, + ): TxFeeState { + getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> + val txFeeResult = getFeeUseCase( + amount = amount.value, + destination = fromToken.value.networkAddress?.defaultAddress ?: "", + userWalletId = userWalletId, + cryptoCurrency = fromToken.currency, + ).firstOrNull() + txFeeResult?.getOrNull()?.let { txFee -> + return txFee.toTxFeeState(networkId) + } + } + return TxFeeState.Empty + } + @Suppress("LongParameterList") private suspend fun updatePermissionState( networkId: String, @@ -759,9 +827,17 @@ internal class SwapInteractorImpl @Inject constructor( data = transactionData, derivationPath = derivationPath, ) - val feeState = proxyFeesToFeeState(networkId, feeData) + val feeState = when (feeData) { + is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId) + is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId) + } + val fee = when (feeState) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue + is TxFeeState.SingleFeeState -> feeState.fee.feeValue + } val isFeeEnough = checkFeeIsEnough( - fee = feeData.normalFee.fee.value, + fee = fee, spendAmount = SwapAmount.zeroSwapAmount(), networkId = networkId, fromToken = fromToken, @@ -801,30 +877,30 @@ internal class SwapInteractorImpl @Inject constructor( } } - private suspend fun proxyFeesToFeeState(networkId: String, proxyFees: ProxyFees): TxFeeState { - val normalFeeValue = proxyFees.minFee.fee.value // in swap for normal use min fee - val normalFeeGas = proxyFees.minFee.gasLimit.toInt() - val priorityFeeValue = proxyFees.normalFee.fee.value // in swap for priority use normal fee - val priorityFeeGas = proxyFees.normalFee.gasLimit.toInt() + private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(networkId: String): TxFeeState { + val normalFeeValue = this.minFee.fee.value // in swap for normal use min fee + val normalFeeGas = this.minFee.gasLimit.toInt() + val priorityFeeValue = this.normalFee.fee.value // in swap for priority use normal fee + val priorityFeeGas = this.normalFee.gasLimit.toInt() val feesFiat = getFormattedFiatFees(networkId, normalFeeValue, priorityFeeValue) val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" } val priorityFiatFee = requireNotNull(feesFiat.getOrNull(1)) { "feesFiat item 1 couldn't be null" } + val networkCurrency = userWalletManager.getNetworkCurrency(networkId) val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = normalFeeValue, decimals = transactionManager.getNativeTokenDecimals(networkId), - currency = userWalletManager.getNetworkCurrency(networkId), ) val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = priorityFeeValue, decimals = transactionManager.getNativeTokenDecimals(networkId), - currency = userWalletManager.getNetworkCurrency(networkId), ) - return TxFeeState( + return TxFeeState.MultipleFeeState( normalFee = TxFee( feeValue = normalFeeValue, gasLimit = normalFeeGas, feeFiatFormatted = normalFiatFee, feeCryptoFormatted = normalCryptoFee, + cryptoSymbol = networkCurrency, feeType = FeeType.NORMAL, ), priorityFee = TxFee( @@ -832,11 +908,108 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = priorityFeeGas, feeFiatFormatted = priorityFiatFee, feeCryptoFormatted = priorityCryptoFee, + cryptoSymbol = networkCurrency, feeType = FeeType.PRIORITY, ), ) } + private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState(networkId: String): TxFeeState { + val normalFeeValue = this.singleFee.fee.value + val normalFeeGas = this.singleFee.gasLimit.toInt() + val networkCurrency = userWalletManager.getNetworkCurrency(networkId) + val feesFiat = getFormattedFiatFees(networkId, normalFeeValue) + val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" } + val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( + amount = normalFeeValue, + decimals = transactionManager.getNativeTokenDecimals(networkId), + ) + return TxFeeState.SingleFeeState( + fee = TxFee( + feeValue = normalFeeValue, + gasLimit = normalFeeGas, + feeFiatFormatted = normalFiatFee, + feeCryptoFormatted = normalCryptoFee, + cryptoSymbol = networkCurrency, + feeType = FeeType.NORMAL, + ), + ) + } + + private suspend fun TransactionFee.toTxFeeState(networkId: String): TxFeeState { + val networkCurrency = userWalletManager.getNetworkCurrency(networkId) + return when (this) { + is TransactionFee.Choosable -> { + val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO + val feePriority = this.priority.amount.value ?: BigDecimal.ZERO + val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0] + val priorityFiatValue = getFormattedFiatFees(networkId, feePriority)[0] + val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( + amount = feeNormal, + decimals = transactionManager.getNativeTokenDecimals(networkId), + ) + val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI( + amount = feePriority, + decimals = transactionManager.getNativeTokenDecimals(networkId), + ) + TxFeeState.MultipleFeeState( + normalFee = TxFee( + feeValue = feeNormal, + gasLimit = this.normal.getGasLimit(), + feeFiatFormatted = normalFiatValue, + feeCryptoFormatted = normalCryptoFee, + cryptoSymbol = networkCurrency, + feeType = FeeType.NORMAL, + ), + priorityFee = TxFee( + feeValue = feePriority, + gasLimit = this.priority.getGasLimit(), + feeFiatFormatted = priorityFiatValue, + feeCryptoFormatted = priorityCryptoFee, + cryptoSymbol = networkCurrency, + feeType = FeeType.PRIORITY, + ), + ) + } + is TransactionFee.Single -> { + val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO + val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0] + val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( + amount = feeNormal, + decimals = transactionManager.getNativeTokenDecimals(networkId), + ) + TxFeeState.SingleFeeState( + fee = TxFee( + feeValue = this.normal.amount.value ?: BigDecimal.ZERO, + gasLimit = this.normal.getGasLimit(), + feeFiatFormatted = normalFiatValue, + feeCryptoFormatted = normalCryptoFee, + cryptoSymbol = networkCurrency, + feeType = FeeType.NORMAL, + ), + ) + } + } + } + + private fun Fee.getGasLimit(): Int { + return when (this) { + is Fee.Common -> 0 + is Fee.Ethereum -> this.gasLimit.toInt() + } + } + + private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): BigDecimal { + return when (txFeeState) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.SingleFeeState -> txFeeState.fee.feeValue + is TxFeeState.MultipleFeeState -> when (feeType) { + FeeType.NORMAL -> txFeeState.normalFee.feeValue + FeeType.PRIORITY -> txFeeState.priorityFee.feeValue + } + } + } + private fun isBalanceEnough( networkId: String, fromToken: CryptoCurrency, @@ -866,7 +1039,7 @@ internal class SwapInteractorImpl @Inject constructor( } } - override suspend fun checkFeeIsEnough( + private suspend fun checkFeeIsEnough( fee: BigDecimal?, spendAmount: SwapAmount, networkId: String, @@ -938,10 +1111,7 @@ internal class SwapInteractorImpl @Inject constructor( } companion object { - private const val DEFAULT_SLIPPAGE = 2 - @Suppress("UnusedPrivateMember") - private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.0 // if need to increase fee when check isEnough private const val INCREASE_GAS_LIMIT_BY = 112 // 12% private const val INFINITY_SYMBOL = "∞" diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index bdcfad48f9..4be138a37f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -5,6 +5,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* @@ -36,6 +37,8 @@ class SwapDomainModule { @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, quotesRepository: QuotesRepository, + walletManagersFacade: WalletManagersFacade, + coroutineDispatcherProvider: CoroutineDispatcherProvider, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -49,6 +52,8 @@ class SwapDomainModule { getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, quotesRepository = quotesRepository, + walletManagersFacade = walletManagersFacade, + dispatcher = coroutineDispatcherProvider, ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt index 30271929e7..89ab7c6754 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt @@ -16,7 +16,7 @@ sealed class ExpressTransactionModel { override val txTo: String, val txFrom: String, val txData: String, - ): ExpressTransactionModel() + ) : ExpressTransactionModel() data class CEX( override val fromAmount: SwapAmount, @@ -25,6 +25,5 @@ sealed class ExpressTransactionModel { override val txTo: String, val externalTxId: String, val externalTxUrl: String, - ): ExpressTransactionModel() - + ) : ExpressTransactionModel() } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt index 5defcf6bf3..01914d161b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt @@ -25,7 +25,7 @@ class AmountFormatter { * @param currency * @return formatted [String] */ - fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String): String { + fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String? = null): String { return amount.toFormattedCurrencyString(decimals, currency) } } \ 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 0dc24830f3..78ccdccca5 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 @@ -20,7 +20,8 @@ sealed interface SwapState { isFeeEnough = false, ), val permissionState: PermissionDataState = PermissionDataState.Empty, - val swapDataModel: SwapStateData? = null, + val swapDataModel: SwapDataModel? = null, + val txFee: TxFeeState, val tangemFee: Double, ) : SwapState @@ -62,21 +63,30 @@ data class RequestApproveStateData( val fromTokenAmount: SwapAmount, ) -data class SwapStateData( - val fee: TxFeeState, - val swapModel: SwapDataModel, -) +// data class SwapStateData( +// val fee: TxFeeState, +// val swapModel: SwapDataModel, +// ) -data class TxFeeState( - val normalFee: TxFee, - val priorityFee: TxFee, -) +sealed class TxFeeState { + data class MultipleFeeState( + val normalFee: TxFee, + val priorityFee: TxFee, + ) : TxFeeState() + + data class SingleFeeState( + val fee: TxFee, + ) : TxFeeState() + + object Empty : TxFeeState() +} data class TxFee( val feeValue: BigDecimal, val gasLimit: Int, val feeFiatFormatted: String, val feeCryptoFormatted: String, + val cryptoSymbol: String, val feeType: FeeType, ) 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 82b62f10c7..a685079175 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 @@ import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.feature.swap.domain.models.ui.TxFee +import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState data class SwapStateHolder( @@ -14,13 +15,14 @@ data class SwapStateHolder( val networkCurrency: String, val networkId: String, val blockchainId: String, // not the same as networkId, its local id in app - val fee: FeeState = FeeState.Empty, val warnings: List = emptyList(), val alert: SwapWarning.GenericWarning? = null, val updateInProgress: Boolean = false, val providerState: ProviderState, + val fee: FeeItemState = FeeItemState.Empty, val permissionState: SwapPermissionState = SwapPermissionState.Empty, + val successState: SwapSuccessStateHolder? = null, val selectTokenState: SwapSelectTokenStateHolder? = null, val bottomSheetConfig: TangemBottomSheetConfig? = 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 7beb38ac1e..76efb65203 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 @@ -1,7 +1,5 @@ package com.tangem.feature.swap.models -import com.tangem.core.ui.components.states.Item -import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.domain.models.ui.TxFee data class UiActions( @@ -17,10 +15,9 @@ data class UiActions( val openPermissionBottomSheet: () -> Unit, val hidePermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, - val onSelectItemFee: (Item) -> Unit, // region new actions val onClickFee: () -> Unit, - val onSelectFeeType: (FeeType) -> Unit, + val onSelectFeeType: (TxFee) -> Unit, val onProviderClick: (String) -> Unit, val onProviderSelect: (String) -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt index 82a1008a27..fddf5f557e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt @@ -4,8 +4,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.feature.swap.domain.models.ui.FeeType import kotlinx.collections.immutable.ImmutableList -class ChooseFeeBottomSheetConfig( +data class ChooseFeeBottomSheetConfig( val selectedFee: FeeType, val onSelectFeeType: (FeeType) -> Unit, - val feeItems: ImmutableList, + val feeItems: ImmutableList, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt index beed7112a6..e01a8b8058 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt @@ -1,13 +1,19 @@ package com.tangem.feature.swap.models.states +import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.swap.domain.models.ui.FeeType -data class FeeItemState( - val feeType: FeeType, - val title: String, - val amountCrypto: String, - val symbolCrypto: String, - val amountFiat: String, - val symbolFiat: String, - val onClick: () -> Unit, -) \ No newline at end of file +sealed class FeeItemState { + + data class Content( + val feeType: FeeType, + val title: TextReference, + val amountCrypto: String, + val symbolCrypto: String, + val amountFiatFormatted: String, + val isClickable: Boolean, + val onClick: () -> Unit, + ) : FeeItemState() + + object Empty : FeeItemState() +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index f967baef74..4556f2d785 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -14,6 +14,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig @@ -31,7 +32,7 @@ fun ChooseFeeBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { Column( - modifier = Modifier.background(TangemTheme.colors.background.secondary), + modifier = Modifier.background(TangemTheme.colors.background.primary), ) { Text( text = "Choose fee", // todo replace with strings @@ -73,7 +74,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { content.feeItems.forEach { feeItem -> val isSelected = feeItem.feeType == content.selectedFee val preEllipsizeText = feeItem.amountCrypto - val postEllipsizeText = " ${feeItem.symbolCrypto} (${feeItem.amountFiat} ${feeItem.symbolFiat})" + val postEllipsizeText = " ${feeItem.symbolCrypto} (${feeItem.amountFiatFormatted})" when (feeItem.feeType) { FeeType.NORMAL -> { SelectorRowItem( @@ -103,22 +104,22 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { @Composable private fun ChooseFeeBottomSheetContent_Preview() { val feeItems = listOf( - FeeItemState( + FeeItemState.Content( feeType = FeeType.NORMAL, - title = "Fee", + title = stringReference("Fee"), amountCrypto = "1000", symbolCrypto = "MATIC", - amountFiat = "10", - symbolFiat = "$", + amountFiatFormatted = "(10$)", + isClickable = false, onClick = {}, ), - FeeItemState( + FeeItemState.Content( feeType = FeeType.PRIORITY, - title = "Fee", + title = stringReference("Fee"), amountCrypto = "2000", symbolCrypto = "MATIC", - amountFiat = "20", - symbolFiat = "$", + amountFiatFormatted = "(10$)", + isClickable = false, onClick = {}, ), ).toImmutableList() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt index c1d2976921..f379ae3511 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt @@ -5,35 +5,47 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.rows.SimpleActionRow +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.FeeItemState @Composable -fun FeeItem(state: FeeItemState) { +fun FeeItemBlock(state: FeeItemState) { + if (state is FeeItemState.Content) { + FeeItem(state = state) + } +} + +@Composable +fun FeeItem(state: FeeItemState.Content) { Box( modifier = Modifier .background( color = TangemTheme.colors.background.action, shape = TangemTheme.shapes.roundedCornersXMedium, ) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) .clickable( onClick = state.onClick, ) .fillMaxWidth() .defaultMinSize(minHeight = TangemTheme.dimens.size68), ) { - val description = "${state.amountCrypto} ${state.symbolCrypto} (${state.amountFiat} ${state.symbolFiat})" + val description = "${state.amountCrypto} ${state.symbolCrypto} (${state.amountFiatFormatted})" SimpleActionRow( modifier = Modifier.padding( start = TangemTheme.dimens.spacing12, top = TangemTheme.dimens.spacing12, ), - title = state.title, + title = state.title.resolveReference(), description = description, + isClickable = state.isClickable, ) } } @@ -41,13 +53,13 @@ fun FeeItem(state: FeeItemState) { @Preview @Composable private fun FeeItemPreview() { - val state = FeeItemState( + val state = FeeItemState.Content( feeType = FeeType.NORMAL, - title = "Fee", + title = stringReference("Fee"), amountCrypto = "1000", symbolCrypto = "MATIC", - amountFiat = "10", - symbolFiat = "$", + amountFiatFormatted = "(1000$)", + isClickable = false, onClick = {}, ) Column { 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 6d740e3fa6..b6a1ec961e 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 @@ -5,8 +5,6 @@ import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.Provider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig -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 @@ -22,9 +20,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* -import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig -import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig -import com.tangem.feature.swap.models.states.ProviderState +import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -76,7 +72,7 @@ internal class StateBuilder( coinId = null, isBalanceHidden = true, ), - fee = FeeState.Loading, + fee = FeeItemState.Empty, networkCurrency = networkInfo.blockchainCurrency, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), onRefresh = {}, @@ -86,7 +82,7 @@ internal class StateBuilder( updateInProgress = true, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, onCancelPermissionBottomSheet = actions.hidePermissionBottomSheet, - providerState = ProviderState.Loading(), + providerState = ProviderState.Empty(), ) } @@ -127,7 +123,7 @@ internal class StateBuilder( ), ), ), - fee = FeeState.Empty, + fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, loading = false, @@ -143,8 +139,8 @@ internal class StateBuilder( toToken: CryptoCurrency, mainTokenId: String, ): SwapStateHolder { - val canSelectSendToken = mainTokenId != fromToken.id.value // TODO look at id matching - val canSelectReceiveToken = mainTokenId != toToken.id.value // TODO look at id matching + val canSelectSendToken = mainTokenId != fromToken.id.value + val canSelectReceiveToken = mainTokenId != toToken.id.value if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder return uiStateHolder.copy( @@ -172,8 +168,9 @@ internal class StateBuilder( balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", isBalanceHidden = isBalanceHiddenProvider(), ), - fee = FeeState.Loading, + fee = FeeItemState.Empty, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), + providerState = ProviderState.Loading(), permissionState = uiStateHolder.permissionState, updateInProgress = true, ) @@ -185,7 +182,6 @@ internal class StateBuilder( * @param uiStateHolder whole screen state * @param quoteModel data model * @param fromToken token data to swap - * @param onFeeSetup callback for reset fee after auto update * @return updated whole screen state */ @Suppress("LongMethod") @@ -194,7 +190,7 @@ internal class StateBuilder( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, swapProvider: SwapProvider, - onFeeSetup: (TxFee) -> Unit, + selectedFeeType: FeeType, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -221,7 +217,7 @@ internal class StateBuilder( ), ) } - val feeState = createFeeState(quoteModel, uiStateHolder, onFeeSetup) + val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus return uiStateHolder.copy( @@ -254,7 +250,6 @@ internal class StateBuilder( permissionState = convertPermissionState( lastPermissionState = uiStateHolder.permissionState, permissionDataState = quoteModel.permissionState, - feeState = feeState, onGivePermissionClick = actions.onGivePermissionClick, onChangeApproveType = actions.onChangeApproveType, ), @@ -308,7 +303,7 @@ internal class StateBuilder( isBalanceHidden = isBalanceHiddenProvider(), ), warnings = emptyList(), - fee = FeeState.Empty, + fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, loading = false, @@ -397,144 +392,36 @@ internal class StateBuilder( } } - fun updateFeeSelectedItem(uiState: SwapStateHolder, item: Item, isFeeEnough: Boolean): SwapStateHolder { - val newSelectedItem = item.copy( - startText = TextReference.Res(R.string.send_network_fee_title), - ) - val permissionState = uiState.permissionState - val newPermissionState = if (permissionState is SwapPermissionState.ReadyForRequest) { - permissionState.copy( - fee = newSelectedItem.endText, - ) - } else { - permissionState - } - val updateState = when (val fee = uiState.fee) { - is FeeState.Loaded -> { - getUpdatedFeeStateForEnoughFee(uiState, fee, item, newSelectedItem, newPermissionState, isFeeEnough) + private fun createFeeState(txFeeState: TxFeeState, feeType: FeeType): FeeItemState { + val isClickable: Boolean + val fee = when (txFeeState) { + TxFeeState.Empty -> return FeeItemState.Empty + is TxFeeState.SingleFeeState -> { + isClickable = false + txFeeState.fee } - is FeeState.NotEnoughFundsWarning -> { - getUpdatedFeeStateForNotEnoughFee(uiState, fee, item, newSelectedItem, newPermissionState, isFeeEnough) + is TxFeeState.MultipleFeeState -> { + isClickable = true + when (feeType) { + FeeType.NORMAL -> { + txFeeState.normalFee + } + FeeType.PRIORITY -> { + txFeeState.priorityFee + } + } } - else -> uiState } - return if (isFeeEnough) { - updateState.copy( - warnings = uiState.warnings.filterNot { it is SwapWarning.InsufficientFunds }, - ) - } else { - updateState.copy( - warnings = uiState.warnings.plus(SwapWarning.InsufficientFunds), - ) - } - } - @Suppress("LongParameterList") - private fun getUpdatedFeeStateForEnoughFee( - uiState: SwapStateHolder, - fee: FeeState.Loaded, - itemToSelect: Item, - newSelectedItem: Item, - newPermissionState: SwapPermissionState, - isFeeEnough: Boolean, - ): SwapStateHolder { - val newState = fee.state?.copy( - selectedItem = newSelectedItem, - items = selectNewItem(fee.state.items, itemToSelect), + return FeeItemState.Content( + feeType = feeType, + title = stringReference("Fee"), // todo replace with string + amountCrypto = fee.feeCryptoFormatted, + symbolCrypto = fee.cryptoSymbol, + amountFiatFormatted = fee.feeFiatFormatted, + isClickable = isClickable, + onClick = actions.onClickFee, ) - val newFeeState = if (isFeeEnough) { - fee.copy(state = newState) - } else { - FeeState.NotEnoughFundsWarning( - tangemFee = fee.tangemFee, - state = newState, - onSelectItem = fee.onSelectItem, - ) - } - return uiState.copy( - fee = newFeeState, - permissionState = newPermissionState, - swapButton = uiState.swapButton.copy( - enabled = isFeeEnough, - ), - ) - } - - @Suppress("LongParameterList") - private fun getUpdatedFeeStateForNotEnoughFee( - uiState: SwapStateHolder, - fee: FeeState.NotEnoughFundsWarning, - itemToSelect: Item, - newSelectedItem: Item, - newPermissionState: SwapPermissionState, - isFeeEnough: Boolean, - ): SwapStateHolder { - val newState = fee.state?.copy( - selectedItem = newSelectedItem, - items = selectNewItem(fee.state.items, itemToSelect), - ) - val newFeeState = if (isFeeEnough) { - FeeState.Loaded( - tangemFee = fee.tangemFee, - state = newState, - onSelectItem = fee.onSelectItem, - ) - } else { - fee.copy(state = newState) - } - return uiState.copy( - fee = newFeeState, - permissionState = newPermissionState, - swapButton = uiState.swapButton.copy( - enabled = isFeeEnough, - ), - ) - } - - private fun createFeeState( - quoteModel: SwapState.QuotesLoadedState, - uiStateHolder: SwapStateHolder, - onFeeSetup: (TxFee) -> Unit, - ): FeeState { - val previousFeeState = when (val stateFee = uiStateHolder.fee) { - is FeeState.Loaded -> stateFee.state - is FeeState.NotEnoughFundsWarning -> stateFee.state - else -> null - } - val permissionState = quoteModel.permissionState - val feeState = if (permissionState is PermissionDataState.PermissionReadyForRequest) { - permissionState.requestApproveData.fee - } else { - quoteModel.swapDataModel?.fee - } - val selectFeeState = createSelectFeeState( - fee = feeState, - previousState = previousFeeState, - onFeeSetup = onFeeSetup, - ) - return if (quoteModel.preparedSwapConfigState.isFeeEnough) { - FeeState.Loaded( - tangemFee = quoteModel.tangemFee, - state = selectFeeState, - onSelectItem = actions.onSelectItemFee, - ) - } else { - FeeState.NotEnoughFundsWarning( - tangemFee = quoteModel.tangemFee, - state = selectFeeState, - onSelectItem = actions.onSelectItemFee, - ) - } - } - - private fun selectNewItem(items: ImmutableList>, selectItem: Item): ImmutableList> { - return items.map { - if (it.id == selectItem.id) { - it.copy(isSelected = true) - } else { - it.copy(isSelected = false) - } - }.toImmutableList() } fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder { @@ -612,7 +499,6 @@ internal class StateBuilder( private fun convertPermissionState( lastPermissionState: SwapPermissionState, permissionDataState: PermissionDataState, - feeState: FeeState, onGivePermissionClick: () -> Unit, onChangeApproveType: (ApproveType) -> Unit, ): SwapPermissionState { @@ -621,97 +507,33 @@ internal class StateBuilder( } else { ApproveType.UNLIMITED } - val fee = when (feeState) { - is FeeSelectState -> feeState.state?.selectedItem?.endText - else -> null - } return when (permissionDataState) { PermissionDataState.Empty -> SwapPermissionState.Empty PermissionDataState.PermissionFailed -> SwapPermissionState.Empty PermissionDataState.PermissionLoading -> SwapPermissionState.InProgress - is PermissionDataState.PermissionReadyForRequest -> SwapPermissionState.ReadyForRequest( - currency = permissionDataState.currency, - amount = permissionDataState.amount, - approveType = approveType, - walletAddress = getShortAddressValue(permissionDataState.walletAddress), - spenderAddress = getShortAddressValue(permissionDataState.spenderAddress), - fee = fee ?: TextReference.Str(""), - approveButton = ApprovePermissionButton( - enabled = true, - onClick = onGivePermissionClick, - ), - cancelButton = CancelPermissionButton( - enabled = true, - ), - onChangeApproveType = onChangeApproveType, - ) - } - } - - private fun createSelectFeeState( - fee: TxFeeState?, - previousState: SelectableItemsState?, - onFeeSetup: (TxFee) -> Unit, - ): SelectableItemsState? { - if (fee == null) return null - if (previousState == null) { - onFeeSetup.invoke(fee.normalFee) // if there is no previous state, setup normal fee by default - val selectedItemId = 0 - // by default preselect normal - val preselectedItem = Item( - id = selectedItemId, - startText = TextReference.Res(R.string.send_network_fee_title), - endText = TextReference.Str(fee.normalFee.feeCryptoFormatted + fee.normalFee.feeFiatFormatted), - isSelected = true, - data = fee.normalFee, - ) - val feeItems = mutableListOf>() - val normalFeeItem = Item( - id = selectedItemId, - startText = TextReference.Res(R.string.send_fee_picker_normal), - endText = TextReference.Str(fee.normalFee.feeCryptoFormatted + fee.normalFee.feeFiatFormatted), - isSelected = true, - data = fee.normalFee, - ) - val priorityFeeItem = Item( - id = 1, - startText = TextReference.Res(R.string.send_fee_picker_priority), - endText = TextReference.Str(fee.priorityFee.feeCryptoFormatted + fee.priorityFee.feeFiatFormatted), - isSelected = false, - data = fee.priorityFee, - ) - feeItems.add(normalFeeItem) - feeItems.add(priorityFeeItem) - return SelectableItemsState( - selectedItem = preselectedItem, - items = feeItems.toImmutableList(), - ) - } else { - val normalFeeItem = - requireNotNull(previousState.items.firstOrNull()) { "in previousState there are 2 items" } - .copy( - endText = TextReference.Str(fee.normalFee.feeCryptoFormatted + fee.normalFee.feeFiatFormatted), - ) - val priorityFeeItem = - requireNotNull(previousState.items.getOrNull(1)) { "in previousState there are 2 items" } - .copy( - endText = TextReference.Str( - fee.priorityFee.feeCryptoFormatted + fee.priorityFee.feeFiatFormatted, - ), - ) - val selectedEndText = if (normalFeeItem.isSelected) { - onFeeSetup.invoke(fee.normalFee) - normalFeeItem.endText - } else { - onFeeSetup.invoke(fee.priorityFee) - priorityFeeItem.endText + is PermissionDataState.PermissionReadyForRequest -> { + val permissionFee = when (val fee = permissionDataState.requestApproveData.fee) { + TxFeeState.Empty -> error("Fee shouldn't be empty") + is TxFeeState.MultipleFeeState -> fee.priorityFee + is TxFeeState.SingleFeeState -> fee.fee + } + SwapPermissionState.ReadyForRequest( + currency = permissionDataState.currency, + amount = permissionDataState.amount, + approveType = approveType, + walletAddress = getShortAddressValue(permissionDataState.walletAddress), + spenderAddress = getShortAddressValue(permissionDataState.spenderAddress), + fee = TextReference.Str("${permissionFee.feeCryptoFormatted} (${permissionFee.feeFiatFormatted})"), + approveButton = ApprovePermissionButton( + enabled = true, + onClick = onGivePermissionClick, + ), + cancelButton = CancelPermissionButton( + enabled = true, + ), + onChangeApproveType = onChangeApproveType, + ) } - return previousState.copy( - selectedItem = previousState.selectedItem.copy( - endText = selectedEndText, - ), - items = listOf(normalFeeItem, priorityFeeItem).toImmutableList(), - ) } } @@ -775,6 +597,70 @@ internal class StateBuilder( } } + fun showSelectFeeBottomSheet( + uiState: SwapStateHolder, + selectedFee: FeeType, + txFeeState: TxFeeState.MultipleFeeState, + onDismiss: () -> Unit, + ): SwapStateHolder { + val config = ChooseFeeBottomSheetConfig( + selectedFee = selectedFee, + onSelectFeeType = { + val selectedItem = when (it) { + FeeType.NORMAL -> txFeeState.normalFee + FeeType.PRIORITY -> txFeeState.priorityFee + } + actions.onSelectFeeType.invoke(selectedItem) + }, + feeItems = txFeeState.toFeeItemState(), + ) + return uiState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismiss, + content = config, + ), + ) + } + + fun updateSelectedFee(uiState: SwapStateHolder, selectedFee: FeeType): SwapStateHolder { + val config = uiState.bottomSheetConfig?.content as? ChooseFeeBottomSheetConfig + return if (config != null) { + uiState.copy( + bottomSheetConfig = uiState.bottomSheetConfig.copy( + content = config.copy( + selectedFee = selectedFee, + ), + ), + ) + } else { + uiState + } + } + + private fun TxFeeState.MultipleFeeState.toFeeItemState(): ImmutableList { + return listOf( + FeeItemState.Content( + feeType = this.normalFee.feeType, + title = stringReference("Fee"), // todo replace with string + amountCrypto = this.normalFee.feeCryptoFormatted, + symbolCrypto = this.normalFee.cryptoSymbol, + amountFiatFormatted = this.normalFee.feeFiatFormatted, + isClickable = true, + onClick = {}, + ), + FeeItemState.Content( + feeType = this.priorityFee.feeType, + title = stringReference("Fee"), // todo replace with string + amountCrypto = this.priorityFee.feeCryptoFormatted, + symbolCrypto = this.priorityFee.cryptoSymbol, + amountFiatFormatted = this.priorityFee.feeFiatFormatted, + isClickable = true, + onClick = {}, + ), + ).toImmutableList() + } + private fun Map.Entry.convertToProviderState( onProviderSelect: (String) -> Unit, ): ProviderState? { 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 95003050c3..a70b6ce6f2 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.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.SwapStateHolder +import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig @@ -32,6 +33,9 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { is ChooseProviderBottomSheetConfig -> { ChooseProviderBottomSheet(config = config) } + is ChooseFeeBottomSheetConfig -> { + ChooseFeeBottomSheet(config = config) + } } } } 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 59a9ab4cfd..8d81d961db 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 @@ -21,20 +21,15 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig -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.getActiveIconRes import com.tangem.core.ui.extensions.getActiveIconResByCoinId import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.ui.FeeType -import com.tangem.feature.swap.domain.models.ui.TxFee import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R -import kotlinx.collections.immutable.toImmutableList -import java.math.BigDecimal @Suppress("LongMethod") @Composable @@ -77,7 +72,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi ), ) - FeeItem(feeState = state.fee, currency = state.networkCurrency) + FeeItemBlock(state = state.fee) if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) @@ -249,49 +244,6 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { } } -@Composable -private fun FeeItem(feeState: FeeState, currency: String) { - val titleString = stringResource(id = R.string.send_network_fee_title) - val disclaimer = stringResource(id = R.string.swapping_tangem_fee_disclaimer, "${feeState.tangemFee}%") - when (feeState) { - is FeeState.Loaded -> { - if (feeState.state != null) { - SelectableInfoCard( - state = feeState.state, - disclaimer = disclaimer, - onSelect = feeState.onSelectItem, - ) - } - } - FeeState.Loading -> { - SmallInfoCardWithDisclaimer( - startText = titleString, - endText = "", - disclaimer = disclaimer, - isLoading = true, - ) - } - is FeeState.NotEnoughFundsWarning -> { - if (feeState.state != null) { - SelectableInfoCardWithWarning( - state = feeState.state, - warningText = stringResource( - id = R.string.swapping_not_enough_funds_for_fee, - currency, - currency, - ), - disclaimer = disclaimer, - onSelect = feeState.onSelectItem, - ) - } - } - is FeeState.Empty -> { - // show nothing - // SmallInfoCard(startText = titleString, endText = "") - } - } -} - @Composable private fun SwapWarnings(warnings: List) { Column( @@ -400,58 +352,18 @@ private val receiveCard = SwapCardState.SwapCardData( isBalanceHidden = false, ) -val stateSelectable = SelectableItemsState( - selectedItem = Item( - 0, - TextReference.Str("Balance"), - TextReference.Str("0.4405434 BTC"), - true, - TxFee( - feeValue = BigDecimal.ZERO, - gasLimit = 0, - feeFiatFormatted = "", - feeCryptoFormatted = "", - feeType = FeeType.NORMAL, - ), - ), - items = listOf( - Item( - 0, - TextReference.Str("Normal"), - TextReference.Str("0.4405434 BTC"), - true, - TxFee( - feeValue = BigDecimal.ZERO, - gasLimit = 0, - feeFiatFormatted = "", - feeCryptoFormatted = "", - feeType = FeeType.NORMAL, - ), - ), - Item( - 1, - TextReference.Str("Priority"), - TextReference.Str("0.46 BTC"), - false, - TxFee( - feeValue = BigDecimal.ZERO, - gasLimit = 0, - feeFiatFormatted = "", - feeCryptoFormatted = "", - feeType = FeeType.NORMAL, - ), - ), - ).toImmutableList(), -) - private val state = SwapStateHolder( networkId = "ethereum", sendCardData = sendCard, receiveCardData = receiveCard, - fee = FeeState.Loaded( - tangemFee = 0.0, - state = stateSelectable, - onSelectItem = {}, + fee = FeeItemState.Content( + feeType = FeeType.NORMAL, + title = stringReference("Fee"), + amountCrypto = "100", + symbolCrypto = "1000", + amountFiatFormatted = "(100)", + isClickable = true, + onClick = {}, ), warnings = listOf( SwapWarning.PermissionNeeded( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index a1da108e5f..191d36f5de 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -1,25 +1,29 @@ package com.tangem.feature.swap.viewmodels import com.tangem.domain.tokens.model.CryptoCurrencyStatus -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.domain.SwapProvider -import com.tangem.feature.swap.domain.models.ui.* +import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData +import com.tangem.feature.swap.domain.models.ui.SwapState +import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress +import com.tangem.feature.swap.domain.models.ui.TxFee data class SwapProcessDataState( // Initial network id val networkId: String, - @Deprecated("used in old swap mechanism") - val fromCurrency: Currency? = null, - @Deprecated("used in old swap mechanism") - val toCurrency: Currency? = null, val fromCryptoCurrency: CryptoCurrencyStatus? = null, val toCryptoCurrency: CryptoCurrencyStatus? = null, // Amount from input val amount: String? = null, val approveDataModel: RequestApproveStateData? = null, - val swapDataModel: SwapStateData? = null, - val selectedFee: TxFee? = null, + val swapDataModel: SwapDataModel? = null, + val selectedFee: TxFee? = null, // todo val tokensDataState: TokensDataStateExpress? = null, val selectedProvider: SwapProvider? = null, val lastLoadedSwapStates: Map = emptyMap(), -) \ No newline at end of file +) { + + fun getCurrentLoadedSwapState(): SwapState.QuotesLoadedState? { + return lastLoadedSwapStates[selectedProvider] as? SwapState.QuotesLoadedState + } +} \ No newline at end of file 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 c70fd5c2a2..41287a8bc6 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 @@ -18,6 +18,7 @@ import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.domain.PermissionOptions +import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* @@ -271,17 +272,14 @@ internal class SwapViewModel @Inject constructor( private fun setupLoadedState(provider: SwapProvider, state: SwapState, fromToken: CryptoCurrencyStatus) { when (state) { is SwapState.QuotesLoadedState -> { - fillDataState(state.permissionState, state.swapDataModel) + fillLoadedDataState(state, state.permissionState, state.swapDataModel) uiState = stateBuilder.createQuotesLoadedState( uiStateHolder = uiState, quoteModel = state, fromToken = fromToken.currency, swapProvider = provider, - ) { updatedFee -> - dataState = dataState.copy( - selectedFee = updatedFee, - ) - } + selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + ) } is SwapState.EmptyAmountState -> { uiState = stateBuilder.createQuotesEmptyAmountState( @@ -305,7 +303,11 @@ internal class SwapViewModel @Inject constructor( return state.entries.first { it.key == selectedSwapProvider }.toPair() } - private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapStateData?) { + private fun fillLoadedDataState( + state: SwapState.QuotesLoadedState, + permissionState: PermissionDataState, + swapDataModel: SwapDataModel?, + ) { dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) { dataState.copy( approveDataModel = permissionState.requestApproveData, @@ -313,11 +315,24 @@ internal class SwapViewModel @Inject constructor( } else { dataState.copy( swapDataModel = swapDataModel, - selectedFee = swapDataModel?.fee?.normalFee, + selectedFee = selectDefaultFee(state), ) } } + private fun selectDefaultFee(state: SwapState.QuotesLoadedState): TxFee? { + return dataState.selectedFee + ?: when (val txFee = state.txFee) { + TxFeeState.Empty -> null + is TxFeeState.MultipleFeeState -> { + txFee.normalFee + } + is TxFeeState.SingleFeeState -> { + txFee.fee + } + } + } + private fun onSwapClick() { singleTaskScheduler.cancelTask() uiState = stateBuilder.createSwapInProgressState(uiState) @@ -326,7 +341,7 @@ internal class SwapViewModel @Inject constructor( swapInteractor.onSwap( exchangeProviderType = requireNotNull(dataState.selectedProvider?.type), networkId = dataState.networkId, - swapStateData = requireNotNull(dataState.swapDataModel), + swapData = requireNotNull(dataState.swapDataModel), currencyToSend = requireNotNull(dataState.fromCryptoCurrency?.currency), currencyToGet = requireNotNull(dataState.toCryptoCurrency?.currency), amountToSwap = requireNotNull(dataState.amount), @@ -539,8 +554,8 @@ internal class SwapViewModel @Inject constructor( onAmountChanged = { onAmountChanged(it) }, onSwapClick = { onSwapClick() - val sendTokenSymbol = dataState.fromCurrency?.symbol - val receiveTokenSymbol = dataState.toCurrency?.symbol + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol if (sendTokenSymbol != null && receiveTokenSymbol != null) { analyticsEventHandler.send( SwapEvents.ButtonSwapClicked( @@ -552,8 +567,8 @@ internal class SwapViewModel @Inject constructor( }, onGivePermissionClick = { givePermissionsToSwap() - val sendTokenSymbol = dataState.fromCurrency?.symbol - val receiveTokenSymbol = dataState.toCurrency?.symbol + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol if (sendTokenSymbol != null && receiveTokenSymbol != null) { analyticsEventHandler.send( SwapEvents.ButtonPermissionApproveClicked( @@ -590,26 +605,36 @@ internal class SwapViewModel @Inject constructor( onChangeApproveType = { approveType -> uiState = stateBuilder.updateApproveType(uiState, approveType) }, - onSelectItemFee = { feeItem -> - dataState = dataState.copy(selectedFee = feeItem.data) - val spendAmount = dataState.amount?.let { amount -> - val fromToken = dataState.fromCryptoCurrency ?: return@let null - swapInteractor.getSwapAmountForToken(amount, fromToken.currency) - } ?: dataState.approveDataModel?.fromTokenAmount - spendAmount ?: return@UiActions - val fromToken = dataState.fromCryptoCurrency ?: return@UiActions - viewModelScope.launch(dispatchers.io) { - val isFeeEnough = swapInteractor.checkFeeIsEnough( - fee = feeItem.data.feeValue, - spendAmount = spendAmount, - networkId = dataState.networkId, - fromToken = fromToken.currency, - ) - uiState = stateBuilder.updateFeeSelectedItem(uiState, feeItem, isFeeEnough) + onClickFee = { + val selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL + val txFeeState = dataState.getCurrentLoadedSwapState()?.txFee as? TxFeeState.MultipleFeeState + ?: return@UiActions + uiState = stateBuilder.showSelectFeeBottomSheet( + uiState = uiState, + selectedFee = selectedFee, + txFeeState = txFeeState, + ) { + uiState = stateBuilder.dismissBottomSheet(uiState) + } + }, + onSelectFeeType = { + val state = dataState.getCurrentLoadedSwapState() ?: return@UiActions + val fromToken = dataState.fromCryptoCurrency ?: return@UiActions + val amountToSwap = dataState.amount ?: return@UiActions + val selectedProvider = dataState.selectedProvider ?: return@UiActions + uiState = stateBuilder.updateSelectedFee(uiState, it.feeType) + dataState = dataState.copy(selectedFee = it) + viewModelScope.launch(dispatchers.io) { + val updatedState = swapInteractor.updateQuotesStateWithSelectedFee( + state = state, + selectedFee = it.feeType, + fromToken = fromToken, + amountToSwap = amountToSwap, + networkId = dataState.networkId, + ) + setupLoadedState(selectedProvider, updatedState, fromToken) } }, - onClickFee = {}, - onSelectFeeType = {}, onProviderClick = { uiState = stateBuilder.showSelectProviderBottomSheet( uiState = uiState, diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt index be151b6dac..b8b3dfa295 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt @@ -1,7 +1,14 @@ package com.tangem.lib.crypto.models -data class ProxyFees( - val minFee: ProxyFee, - val normalFee: ProxyFee, - val priorityFee: ProxyFee, -) \ No newline at end of file +sealed class ProxyFees { + + data class MultipleFees( + val minFee: ProxyFee, + val normalFee: ProxyFee, + val priorityFee: ProxyFee, + ) : ProxyFees() + + data class SingleFee( + val singleFee: ProxyFee, + ) : ProxyFees() +} \ No newline at end of file