diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9a66725eb7..e3f7a4fcde 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.domain.appTheme.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.transaction) implementation(projects.common) implementation(projects.core.analytics) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt new file mode 100644 index 0000000000..5b68512d77 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +internal object TransactionDomainModule { + + @Provides + @ViewModelScoped + fun provideGetUseCase( + walletManagersFacade: WalletManagersFacade, + dispatchers: CoroutineDispatcherProvider, + ): GetFeeUseCase { + return GetFeeUseCase(walletManagersFacade, dispatchers) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index 78084743b7..74815a675f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -18,7 +18,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item -import com.tangem.tap.features.details.ui.common.TangemSwitch +import com.tangem.core.ui.components.TangemSwitch @Composable internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b26afb1dea..6bd6a2445a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -421,7 +421,7 @@ Priority Check your network connection Network fee info unreachable - From **%s** + From Gas limit Gas Limit is auto-calculated; raise it during network congestion Gas price diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index 9839280049..c4d41ef91b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.details.ui.common +package com.tangem.core.ui.components import androidx.compose.animation.animateColor import androidx.compose.animation.core.* diff --git a/core/ui/src/main/res/drawable/ic_bird_24.xml b/core/ui/src/main/res/drawable/ic_bird_24.xml new file mode 100644 index 0000000000..234ea4f693 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_bird_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_edit_24.xml b/core/ui/src/main/res/drawable/ic_edit_24.xml new file mode 100644 index 0000000000..cc2504eb91 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_edit_24.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_hare_24.xml b/core/ui/src/main/res/drawable/ic_hare_24.xml new file mode 100644 index 0000000000..e2714dc9d0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_hare_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_tortoise_24.xml b/core/ui/src/main/res/drawable/ic_tortoise_24.xml new file mode 100644 index 0000000000..8dee215fd1 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tortoise_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index bc1bf00b14..a581d0d2fa 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -10,6 +10,7 @@ import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult @@ -411,6 +412,24 @@ class DefaultWalletManagersFacade( } } + override suspend fun getFee( + amount: Amount, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): Result? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return (walletManager as? TransactionSender)?.getFee( + amount = amount, + destination = destination, + ) + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 31345d6f99..8e830e0bdd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -2,10 +2,13 @@ package com.tangem.domain.walletmanager import arrow.core.Either import com.tangem.blockchain.blockchains.solana.RentProvider +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -143,4 +146,19 @@ interface WalletManagersFacade { network: Network, signedHashes: Int, ): Either + + /** + * Returns fee for transaction + * + * @param amount of transaction + * @param destination address + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun getFee( + amount: Amount, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): Result? } \ No newline at end of file diff --git a/domain/transaction/.gitignore b/domain/transaction/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/transaction/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts new file mode 100644 index 0000000000..8da795757a --- /dev/null +++ b/domain/transaction/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.transaction" +} + +dependencies { + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + + implementation(projects.core.utils) + + implementation(deps.tangem.blockchain) + + implementation(projects.domain.legacy) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt new file mode 100644 index 0000000000..f08011b2d4 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.transaction.error + +sealed class GetFeeError { + object DataError : GetFeeError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt new file mode 100644 index 0000000000..dee4c3cdce --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt @@ -0,0 +1,65 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import java.math.BigDecimal + +/** + * Use case to get transaction fee + */ +class GetFeeUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val dispatcher: CoroutineDispatcherProvider, +) { + suspend operator fun invoke( + amount: BigDecimal, + destination: String, + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Flow> { + return flow { + val result = walletManagersFacade.getFee( + amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), + destination = destination, + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) + + val maybeFee = when (result) { + is Result.Success -> result.data.right() + else -> GetFeeError.DataError.left() + } + emit(maybeFee) + }.flowOn(dispatcher.io) + } + + private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = amount, + decimals = cryptoCurrency.decimals, + type = when (cryptoCurrency) { + is CryptoCurrency.Coin -> AmountType.Coin + is CryptoCurrency.Token -> AmountType.Token( + token = Token( + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) + }, + ) +} \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 65f362206d..bb59a8e295 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) + implementation(projects.domain.transaction) /** Feature modules */ implementation(projects.features.send.api) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 87ac690720..3bb47678a9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.state import androidx.paging.PagingData import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference @@ -12,6 +13,10 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.fee.SendFeeCustomFieldConverter +import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter @@ -35,8 +40,13 @@ internal class SendStateFactory( private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) } - private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) } + private val customFeeFieldConverter by lazy { + SendFeeCustomFieldConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + ) + } private val amountStateConverter by lazy { SendAmountStateConverter( @@ -47,13 +57,17 @@ internal class SendStateFactory( cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } - private val recipientStateConverter by lazy { SendRecipientStateConverter( clickIntents = clickIntents, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } + private val feeStateConverter by lazy { + SendFeeStateConverter( + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } private val recipientListStateConverter by lazy { SendRecipientListConverter( @@ -71,7 +85,7 @@ internal class SendStateFactory( fun getReadyState(): SendUiState = currentStateProvider().copy( amountState = amountStateConverter.convert(Unit), recipientState = recipientStateConverter.convert(Unit), - feeState = SendStates.FeeState(), + feeState = feeStateConverter.convert(Unit), ) //endregion @@ -164,5 +178,28 @@ internal class SendStateFactory( ), ) } + + fun onFeeOnLoadingState() { + currentStateProvider().feeState?.feeSelectorState?.update { + FeeSelectorState.Loading + } + } + + fun onFeeOnLoadedState(fees: TransactionFee) { + currentStateProvider().feeState?.feeSelectorState?.update { + FeeSelectorState.Content( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + ) + } + } + //endregion + + //region fee + fun onFeeSelectedState(feeType: FeeType) { + currentStateProvider().feeState?.feeSelectorState?.update { + (it as? FeeSelectorState.Content)?.copy(selectedFee = feeType) ?: it + } + } //endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 6baeb27cfb..207e63064d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -8,6 +8,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.PersistentList @@ -55,10 +56,13 @@ internal sealed class SendStates { val isPrimaryButtonEnabled: Boolean, ) : SendStates() - // todo [REDACTED_JIRA] /** Fee and speed state */ data class FeeState( override val type: SendUiStateType = SendUiStateType.Fee, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val feeSelectorState: MutableStateFlow = MutableStateFlow(FeeSelectorState.Empty), + val isSubtract: MutableStateFlow = MutableStateFlow(false), + val receivedAmount: MutableStateFlow = MutableStateFlow(""), ) : SendStates() // todo [REDACTED_JIRA] diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index e7526fdf04..e50ae05ae8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -16,15 +16,11 @@ internal class StateRouter( fun onNextClick() { when (currentState.value) { - SendUiStateType.Amount -> { - currentState.update { SendUiStateType.Recipient } - } - SendUiStateType.Recipient -> { - currentState.update { SendUiStateType.Fee } - } - else -> { - // todo implement - } + SendUiStateType.Amount -> currentState.update { SendUiStateType.Recipient } + SendUiStateType.Recipient -> currentState.update { SendUiStateType.Fee } + SendUiStateType.Fee -> currentState.update { SendUiStateType.Send } + SendUiStateType.Send -> currentState.update { SendUiStateType.Done } + SendUiStateType.Done -> onBackClick() } } @@ -33,7 +29,8 @@ internal class StateRouter( SendUiStateType.Amount -> onBackClick() SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount } SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient } - else -> onBackClick() + SendUiStateType.Send -> currentState.update { SendUiStateType.Fee } + SendUiStateType.Done -> onBackClick() } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt new file mode 100644 index 0000000000..f920348ac8 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt @@ -0,0 +1,27 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import androidx.compose.runtime.Immutable +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import kotlinx.coroutines.flow.MutableStateFlow + +@Immutable +internal sealed class FeeSelectorState { + + object Loading : FeeSelectorState() + + object Empty : FeeSelectorState() + + data class Content( + val fees: TransactionFee, + val selectedFee: FeeType = FeeType.MARKET, + val customValues: MutableStateFlow> = MutableStateFlow(emptyList()), + ) : FeeSelectorState() +} + +enum class FeeType { + SLOW, + MARKET, + FAST, + CUSTOM, +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt new file mode 100644 index 0000000000..39851a417a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -0,0 +1,61 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow + +internal class SendFeeCustomFieldConverter( + private val clickIntents: SendClickIntents, + private val appCurrencyProvider: Provider, +) : Converter>> { + + override fun convert(value: Fee): MutableStateFlow> { + val ethereumFee = value as? Fee.Ethereum ?: return MutableStateFlow(emptyList()) + val appCurrency = appCurrencyProvider() + + val maxFeeFiat = BigDecimalFormatter.formatFiatAmount( + fiatAmount = ethereumFee.amount.value, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + + return MutableStateFlow( + listOf( + SendTextField.CustomFee( + value = ethereumFee.amount.value.toString(), + onValueChange = { clickIntents.onCustomFeeValueChange(0, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + label = TextReference.Str(maxFeeFiat), + ), + SendTextField.CustomFee( + value = ethereumFee.gasPrice.toString(), + onValueChange = { clickIntents.onCustomFeeValueChange(1, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + ), + SendTextField.CustomFee( + value = ethereumFee.gasLimit.toString(), + onValueChange = { clickIntents.onCustomFeeValueChange(2, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt new file mode 100644 index 0000000000..13a2ebe5c2 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -0,0 +1,17 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import com.tangem.common.Provider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.utils.converter.Converter + +internal class SendFeeStateConverter( + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + + override fun convert(value: Unit): SendStates.FeeState { + return SendStates.FeeState( + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 93a0f62d57..c8f289df54 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -20,6 +20,7 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent +import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent @Composable @@ -89,6 +90,10 @@ private fun SendScreenContent( uiState.clickIntents, recipientList, ) + SendUiStateType.Fee -> SendSpeedAndFeeContent( + uiState.feeState, + uiState.clickIntents, + ) else -> { /* [REDACTED_TODO_COMMENT]*/ } } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt index ce0fbcee3b..26b442e62a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.common +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -25,9 +26,9 @@ internal fun FooterContainer( ) { Column(modifier = modifier) { content() - footer?.let { + AnimatedVisibility(visible = footer != null) { Text( - text = it, + text = footer.orEmpty(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt new file mode 100644 index 0000000000..a96d8c5f1b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt @@ -0,0 +1,63 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.fields.AmountVisualTransformation +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.ui.recipient.TextFieldWithInfo + +private const val ETHEREUM_UNIT = "GWEI" + +@Composable +internal fun SendCustomFeeEthereum( + customValues: State>, + selectedFee: FeeType, + symbol: String, + modifier: Modifier = Modifier, +) { + val fee = customValues.value[0] + val gasPrice = customValues.value[1] + val gasLimit = customValues.value[2] + + if (selectedFee == FeeType.CUSTOM && customValues.value.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = modifier, + ) { + TextFieldWithInfo( + value = fee.value, + label = stringResource(R.string.send_max_fee), + footer = stringResource(R.string.send_max_fee_footer), + info = fee.label, + visualTransformation = AmountVisualTransformation(symbol), + keyboardOptions = fee.keyboardOptions, + onValueChange = fee.onValueChange, + isSingleLine = true, + ) + TextFieldWithInfo( + value = gasPrice.value, + label = stringResource(R.string.send_gas_price), + footer = stringResource(R.string.send_gas_price_footer), + onValueChange = gasPrice.onValueChange, + visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT), + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + ) + TextFieldWithInfo( + value = gasLimit.value, + label = stringResource(R.string.send_gas_limit), + footer = stringResource(R.string.send_gas_limit_footer), + onValueChange = gasLimit.onValueChange, + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt new file mode 100644 index 0000000000..595d04491a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -0,0 +1,86 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents + +private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY" +private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) { + if (state == null) return + val feeSendState = state.feeSelectorState.collectAsStateWithLifecycle() + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary) + .padding( + horizontal = TangemTheme.dimens.spacing16, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + item( + key = FEE_SELECTOR_KEY, + ) { + SendSpeedSelector( + state = feeSendState, + clickIntents = clickIntents, + ) + } + if (feeSendState.value is FeeSelectorState.Content) { + item( + key = FEE_CUSTOM_KEY, + ) { + AnimatedVisibility( + visible = feeSendState.value is FeeSelectorState.Content, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.tertiary), + ) { + val fee = feeSendState.value as FeeSelectorState.Content + val customValues = fee.customValues.collectAsStateWithLifecycle() + SendCustomFeeEthereum( + customValues = customValues, + selectedFee = fee.selectedFee, + symbol = state.cryptoCurrencyStatus.currency.symbol, + modifier = Modifier + .animateItemPlacement(), + ) + } + } + } + item { + val topPadding = (feeSendState.value as? FeeSelectorState.Content)?.let { state -> + if (state.selectedFee != FeeType.CUSTOM) { + TangemTheme.dimens.spacing8 + } else { + TangemTheme.dimens.spacing0 + } + } ?: TangemTheme.dimens.spacing0 + + SendSpeedSubtract( + receivingAmount = state.receivedAmount, + isSubtract = state.isSubtract, + onSelectClick = clickIntents::onSubtractSelect, + modifier = Modifier + .animateItemPlacement() + .padding( + top = topPadding, + bottom = TangemTheme.dimens.spacing12, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt new file mode 100644 index 0000000000..de69e011d6 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -0,0 +1,271 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents + +@Suppress("LongMethod") +@Composable +internal fun SendSpeedSelector( + state: State, + clickIntents: SendClickIntents, + modifier: Modifier = Modifier, +) { + FooterContainer( + footer = stringResource(R.string.common_fee_selector_footer), + modifier = modifier, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + when (val selector = state.value) { + FeeSelectorState.Loading -> { + SendSpeedSelectorItemLoading() + SendSpeedSelectorItemLoading() + SendSpeedSelectorItemLoading() + } + is FeeSelectorState.Content -> { + when (selector.fees) { + is TransactionFee.Choosable -> { + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_slow, + iconRes = R.drawable.ic_tortoise_24, + amount = TextReference.Str(selector.fees.minimum.amount.value.toString()), + symbol = TextReference.Str(selector.fees.minimum.amount.currencySymbol), + isSelected = selector.selectedFee == FeeType.SLOW, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.SLOW) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + amount = TextReference.Str(selector.fees.normal.amount.value.toString()), + symbol = TextReference.Str(selector.fees.normal.amount.currencySymbol), + isSelected = selector.selectedFee == FeeType.MARKET, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_fast, + iconRes = R.drawable.ic_hare_24, + amount = TextReference.Str(selector.fees.priority.amount.value.toString()), + symbol = TextReference.Str(selector.fees.priority.amount.currencySymbol), + isSelected = selector.selectedFee == FeeType.FAST, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) }, + showDivider = selector.fees.normal is Fee.Ethereum, + ) + if (selector.fees.normal is Fee.Ethereum) { + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_custom, + iconRes = R.drawable.ic_edit_24, + isSelected = selector.selectedFee == FeeType.CUSTOM, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.CUSTOM) }, + showDivider = selector.fees.normal !is Fee.Ethereum, + ) + } + } + is TransactionFee.Single -> { + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + isSelected = true, + amount = TextReference.Str(selector.fees.normal.amount.value.toString()), + symbol = TextReference.Str(selector.fees.normal.amount.currencySymbol), + onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, + showDivider = false, + ) + } + } + } + FeeSelectorState.Empty -> Unit + } + } + } +} + +@Composable +private fun SendSpeedSelectorItemLoading() { + Row(modifier = Modifier.fillMaxWidth()) { + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing18, + bottom = TangemTheme.dimens.spacing18, + start = TangemTheme.dimens.spacing12, + ) + .size( + width = TangemTheme.dimens.size50, + height = TangemTheme.dimens.size12, + ), + ) + SpacerWMax() + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing18, + bottom = TangemTheme.dimens.spacing18, + end = TangemTheme.dimens.spacing12, + ) + .size( + width = TangemTheme.dimens.size90, + height = TangemTheme.dimens.size12, + ), + ) + } +} + +@Composable +private fun SendSpeedSelectorItem( + @StringRes titleRes: Int, + @DrawableRes iconRes: Int, + onSelect: () -> Unit, + modifier: Modifier = Modifier, + amount: TextReference? = null, + symbol: TextReference? = null, + isSelected: Boolean = false, + showDivider: Boolean = true, +) { + val iconTint by animateColorAsState( + targetValue = if (isSelected) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.icon.informative + }, + label = "Selector icon tint change", + ) + + val textStyle = if (isSelected) { + TangemTheme.typography.subtitle2 + } else { + TangemTheme.typography.body2 + } + + Box( + modifier = modifier + .fillMaxWidth() + .clickable { onSelect() }, + ) { + Row(modifier = Modifier.fillMaxWidth()) { + Icon( + painter = painterResource(iconRes), + tint = iconTint, + contentDescription = null, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) + Text( + text = stringResource(titleRes), + style = textStyle, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) + if (amount != null && symbol != null) { + SelectorValueContent( + amount = amount, + symbol = symbol, + textStyle = textStyle, + ) + } + } + if (showDivider) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size1) + .padding(horizontal = TangemTheme.dimens.spacing12) + .background(TangemTheme.colors.stroke.primary) + .align(Alignment.BottomCenter), + ) + } + } +} + +@Composable +private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) { + Text( + text = amount.resolveReference(), + style = textStyle, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = Modifier + .weight(1f) + .padding( + start = TangemTheme.dimens.spacing4, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) + Text( + text = symbol.resolveReference(), + style = textStyle, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing1, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) +} + +//region preview +@Preview +@Composable +private fun FeeSelectorPreview_Light() { + TangemTheme { + SendSpeedSelectorItemLoading() + } +} + +@Preview +@Composable +private fun FeeSelectorPreview_Dark() { + TangemTheme(isDark = true) { + SendSpeedSelectorItemLoading() + } +} +//endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt new file mode 100644 index 0000000000..fac746da5b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt @@ -0,0 +1,66 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer +import kotlinx.coroutines.flow.StateFlow + +@Composable +internal fun SendSpeedSubtract( + receivingAmount: StateFlow, + isSubtract: StateFlow, + onSelectClick: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val isSelected = isSubtract.collectAsStateWithLifecycle() + val footer = receivingAmount.collectAsStateWithLifecycle() + + val footerText = if (isSelected.value) { + stringResource(R.string.send_amount_substract_footer, footer.value) + } else { + null + } + + FooterContainer( + footer = footerText, + modifier = modifier, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing20, + ), + ) { + Text( + text = stringResource(R.string.send_amount_substract), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(end = TangemTheme.dimens.spacing12), + ) + TangemSwitch( + checked = isSelected.value, + onCheckedChange = onSelectClick, + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt index 3a2ed57e1a..f2a792f03e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -175,7 +176,9 @@ internal fun TextFieldWithInfo( modifier: Modifier = Modifier, info: TextReference? = null, footer: String? = null, + isSingleLine: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, ) { FooterContainer( footer = footer, @@ -206,6 +209,8 @@ internal fun TextFieldWithInfo( value = value, onValueChange = onValueChange, visualTransformation = visualTransformation, + singleLine = isSingleLine, + keyboardOptions = keyboardOptions, modifier = Modifier .padding(top = TangemTheme.dimens.spacing6) .weight(1f), @@ -284,15 +289,17 @@ private fun SimpleTextField( placeholder: TextReference? = null, singleLine: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, ) { val focusRequester = remember { FocusRequester() } BasicTextField( value = value, onValueChange = onValueChange, - textStyle = TangemTheme.typography.body2, + textStyle = TangemTheme.typography.body2.copy(color = TangemTheme.colors.text.primary1), cursorBrush = SolidColor(TangemTheme.colors.text.primary1), singleLine = singleLine, visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, decorationBox = { textValue -> Box { if (value.isBlank() && placeholder != null) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 03740cb51e..35df0a6614 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -1,5 +1,7 @@ package com.tangem.features.send.impl.presentation.viewmodel +import com.tangem.features.send.impl.presentation.state.fee.FeeType + interface SendClickIntents { fun onBackClick() @@ -23,4 +25,12 @@ interface SendClickIntents { fun onRecipientMemoValueChange(value: String) // endregion + + // region Fee + fun onFeeSelectorClick(feeType: FeeType) + + fun onCustomFeeValueChange(index: Int, value: String) + + fun onSubtractSelect(value: Boolean) + // endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 2bc9bb638a..3e8bc7477f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -9,13 +9,16 @@ import arrow.core.getOrElse import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.Provider +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -27,7 +30,10 @@ import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.SendStateFactory import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.StateRouter +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -37,6 +43,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates @@ -50,6 +57,7 @@ internal class SendViewModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val getFeeUseCase: GetFeeUseCase, private val walletManagersFacade: WalletManagersFacade, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -84,11 +92,13 @@ internal class SendViewModel @Inject constructor( private var balanceJobHolder = JobHolder() private var recipientsJobHolder = JobHolder() private var walletAddressesJobHolder = JobHolder() + private var feeJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { getWalletAddresses() subscribeOnCurrencyStatusUpdates(owner) getWalletsAndRecent() + getFee() } fun setRouter(router: StateRouter) { @@ -203,6 +213,38 @@ internal class SendViewModel @Inject constructor( } } + private fun getFee() { + viewModelScope.launch(dispatchers.main) { + uiState.currentState + .filter { it == SendUiStateType.Fee } + .onEach { + val amountState = uiState.amountState ?: return@onEach + val recipientState = uiState.recipientState ?: return@onEach + + stateFactory.onFeeOnLoadingState() + getFeeUseCase.invoke( + amount = amountState.amountTextField.value.value.toBigDecimal(), + destination = recipientState.addressTextField.value.value, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ) + .conflate() + .distinctUntilChanged() + .onEach { maybeFee -> + maybeFee.fold( + ifRight = { + stateFactory.onFeeOnLoadedState(it) + }, + ifLeft = { + // TODO add error handling + }, + ) + } + .launchIn(viewModelScope) + }.launchIn(viewModelScope) + }.saveIn(feeJobHolder) + } + private fun getWalletAddresses() { viewModelScope.launch(dispatchers.io) { walletAddresses = walletManagersFacade.getAddresses( @@ -268,6 +310,62 @@ internal class SendViewModel @Inject constructor( } // endregion + //region fee + override fun onFeeSelectorClick(feeType: FeeType) { + stateFactory.onFeeSelectedState(feeType) + updateReceiveAmount() + } + + override fun onCustomFeeValueChange(index: Int, value: String) { + uiState.feeState?.apply { + (feeSelectorState.value as? FeeSelectorState.Content)?.let { feeSelector -> + feeSelector.customValues.update { + it.toMutableList().apply { + set(index, it[index].copy(value = value)) + } + } + updateReceiveAmount() + } + } + } + + override fun onSubtractSelect(value: Boolean) { + uiState.feeState?.isSubtract?.update { value } + if (value) { + updateReceiveAmount() + } + } + + private fun updateReceiveAmount() { + uiState.feeState?.receivedAmount?.update { + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = calculateReceiveAmount(), + cryptoCurrency = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ) + } + } + + private fun calculateReceiveAmount(): BigDecimal { + val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return BigDecimal.ZERO + val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO + + val fee = when (val selectedFee = feeState.fees) { + is TransactionFee.Choosable -> { + when (feeState.selectedFee) { + FeeType.SLOW -> selectedFee.minimum.amount.value + FeeType.MARKET -> selectedFee.normal.amount.value + FeeType.FAST -> selectedFee.priority.amount.value + FeeType.CUSTOM -> feeState.customValues.value.firstOrNull()?.value?.let { BigDecimal(it) } + } + } + is TransactionFee.Single -> selectedFee.normal.amount.value + } ?: BigDecimal.ZERO + + return BigDecimal(amount.value).minus(fee) + } + //endregion + companion object { private const val XRP_X_ADDRESS = 'X' private const val DEFAULT_VALUE = "0.00" diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 86e6d89da1..eed86ea134 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-384" +tangemBlockchainSdk = "develop-387" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-310" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds diff --git a/settings.gradle.kts b/settings.gradle.kts index 2bda17e30f..59ac1ba906 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -127,6 +127,7 @@ include(":domain:app-theme") include(":domain:app-theme:models") include(":domain:balance-hiding") include(":domain:balance-hiding:models") +include(":domain:transaction") // endregion Domain modules