Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-15 18:35:58 +03:00
parent 31d5c67091
commit d214ec6ec6
33 changed files with 963 additions and 21 deletions

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -421,7 +421,7 @@
<string name="send_fee_picker_priority">Priority</string>
<string name="send_fee_unreachable_error_text">Check your network connection</string>
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
<string name="send_from_wallet">From **%s**</string>
<string name="send_from_wallet_android">From</string>
<string name="send_gas_limit">Gas limit</string>
<string name="send_gas_limit_footer">Gas Limit is auto-calculated; raise it during network congestion</string>
<string name="send_gas_price">Gas price</string>

View file

@ -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.*

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15.316,6.992C14.311,6.992 13.538,7.568 12.995,8.794C10.963,6.753 7.995,5.045 5.043,4.806C4.152,4.746 4.031,5.519 4.011,5.98C3.816,9.464 6.319,12.606 9.521,13.127C8.61,15.057 7.359,16.684 5.776,18.093C5.387,18.421 5.545,18.934 5.727,19.241C6.61,20.688 9.408,21.692 11.137,21.056C11.667,20.863 11.74,20.513 11.74,20.163V17.849C13.534,17.653 16.32,16.201 15.927,12.273L15.899,11.979C17.227,11.573 18.158,10.6 18.158,9.549C18.158,8.298 16.858,6.992 15.316,6.992ZM15.389,8.388C16.219,8.388 16.867,9.011 16.867,9.549C16.867,10.015 16.316,10.557 15.008,10.749C14.68,10.792 14.49,11.018 14.514,11.33L14.579,12.299C14.809,15.795 12.283,16.265 10.854,16.389C10.558,16.415 10.364,16.641 10.364,16.931V19.736C10.364,19.851 10.339,19.916 10.202,19.928C9.278,20.039 7.687,19.442 7.149,18.81C7.08,18.72 7.08,18.643 7.132,18.596C8.837,17.072 10.214,15.095 11.149,12.747C11.34,12.29 11.121,11.898 10.7,11.893C7.768,11.868 5.549,9.575 5.387,6.45C5.383,6.326 5.428,6.3 5.533,6.322C8.145,6.736 10.574,8.303 12.085,9.848C12.437,10.207 12.667,10.361 13,10.361C13.538,10.361 13.805,10.096 14.081,9.498C14.368,8.862 14.656,8.388 15.389,8.388ZM13.996,8.26L15.283,7.782C14.344,5.408 13.016,3.769 11.242,2.68C10.586,2.283 9.918,2.56 9.732,3.342C9.4,4.691 9.258,5.912 9.449,7.218L10.987,7.995C10.655,6.714 10.663,5.647 10.902,4.418C10.922,4.285 11.028,4.242 11.129,4.319C12.34,5.22 13.255,6.458 13.996,8.26ZM16.749,10.77L19.417,11.185C19.988,11.27 20.256,10.408 19.677,10.143L17.263,9.058L16.749,10.77Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,7 @@
<vector android:height="24dp" android:viewportHeight="25"
android:viewportWidth="25" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0.676,0.968h24v24h-24z"/>
<path android:fillColor="#909090" android:pathData="M5.676,19.968H6.938L17.174,9.731L15.913,8.47L5.676,18.706V19.968ZM4.176,21.468V18.083L17.366,4.899C17.518,4.761 17.685,4.655 17.867,4.58C18.05,4.505 18.242,4.468 18.442,4.468C18.643,4.468 18.837,4.503 19.025,4.575C19.213,4.646 19.379,4.759 19.524,4.914L20.745,6.151C20.9,6.295 21.011,6.462 21.077,6.651C21.143,6.839 21.176,7.028 21.176,7.216C21.176,7.417 21.142,7.609 21.073,7.792C21.004,7.975 20.895,8.142 20.745,8.293L7.561,21.468H4.176ZM16.532,9.112L15.913,8.47L17.174,9.731L16.532,9.112Z"/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15.564,18.109C16.941,18.109 17.59,17.357 17.59,16.486C17.59,14.706 16.348,14.153 14.183,14.153C13.347,14.153 13.085,14.205 12.874,14.253L12.432,13.087C11.6,10.885 10.06,9.95 8.352,9.95C8.097,9.95 7.799,9.95 7.493,10.018C7.071,10.109 6.896,10.288 6.896,10.555C6.896,10.937 7.186,11.2 7.58,11.196C7.815,11.196 8.066,11.156 8.336,11.156C9.682,11.156 10.601,11.916 11.389,13.819L12.078,15.51C12.619,15.403 13.184,15.307 14.135,15.307C15.262,15.307 16.121,15.67 16.121,16.314C16.121,16.561 15.934,16.776 15.564,16.776C14.919,16.776 14.438,16.633 13.821,16.633C13.16,16.633 12.424,16.697 12.189,16.697C11.922,16.697 11.715,16.681 11.512,16.493L8.667,13.711C6.593,13.827 5.268,13.341 4.452,12.211C3.887,12.255 3.465,12.056 3.465,11.646C3.465,11.228 3.918,10.981 4.519,10.981C5.148,9.003 6.322,8.107 8.054,8.107C10.044,8.107 11.767,9.648 13.574,10.543C14.115,10.802 14.712,10.921 15.071,10.921C16.034,10.921 16.702,10.356 16.917,9.305L12.858,6.539C12.555,6.336 12.209,6.05 12.209,5.902C12.209,5.584 12.814,5.401 13.295,5.401C14.207,5.401 14.896,5.879 15.588,6.671L17.705,9.102C19.504,9.106 20.667,10.372 20.667,11.988C20.667,12.935 20.249,13.206 18.983,13.206C18.259,13.206 17.343,12.931 16.476,12.462C15.226,12.776 13.916,13.465 13.092,14.281L14.235,14.691C14.971,14.217 15.779,13.891 16.316,13.684C17.212,14.197 18.195,14.42 18.947,14.416C20.901,14.408 22,13.707 22,11.98C22,9.795 20.44,8.167 18.318,8.048L17.033,6.209C16.034,4.788 14.876,4 13.28,4C12.113,4 10.601,4.633 10.601,5.767C10.601,6.48 11.174,6.933 11.994,7.479C12.754,7.98 14.398,9.003 15.278,9.556C15.21,9.596 15.126,9.628 15.027,9.628C14.792,9.628 14.362,9.465 14.056,9.258C11.648,7.992 10.104,6.675 8.066,6.675C5.829,6.675 4.197,7.845 3.417,10.149C2.537,10.308 2,10.906 2,11.753C2,12.776 2.792,13.377 3.755,13.389C4.364,14.424 6.004,15.256 8.03,15.097L10.573,17.52C10.983,17.922 11.5,18.07 12.161,18.07C12.706,18.07 13.192,17.942 13.861,17.942C14.557,17.942 14.999,18.109 15.564,18.109ZM7.389,18.838C8.826,18.838 10.247,18.352 11.063,17.385L10.107,16.593C9.69,17.234 8.639,17.624 7.429,17.624C6.649,17.624 6.088,17.369 6.088,16.927C6.088,16.72 6.243,16.553 6.529,16.553C6.907,16.553 7.282,16.661 7.755,16.661C8.499,16.661 9.132,16.549 9.63,16.115L8.758,15.252C8.452,15.467 8.078,15.526 7.759,15.526C7.158,15.522 6.808,15.403 6.326,15.403C5.339,15.403 4.675,16.004 4.675,16.907C4.675,18.113 5.761,18.838 7.389,18.838ZM18.537,11.622C18.876,11.622 19.15,11.347 19.15,11.009C19.15,10.675 18.876,10.396 18.537,10.396C18.203,10.396 17.924,10.675 17.924,11.009C17.924,11.347 18.203,11.622 18.537,11.622Z"
android:fillColor="#909090"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.408,17.317C11.952,17.317 12.77,15.774 14.392,15.699C16.461,15.551 17.283,14.326 18.957,14.326C20.756,14.326 22,13.667 22,12.497C22,10.076 20.634,8.665 18.879,8.665C17.231,8.665 16.213,9.635 15.865,11.172C15.739,11.701 15.506,12.038 15.177,12.267L14.943,11.842C13.348,8.917 11.848,7.732 9.416,7.725C6.906,7.732 5.469,8.876 3.703,12.179C2.985,13.53 2,13.708 2,14.618C2,15.277 2.529,15.699 3.462,15.699C3.666,15.699 3.881,15.699 4.088,15.699C6.002,15.699 7.043,17.317 9.408,17.317ZM9.408,15.87C7.513,15.87 6.332,14.267 4.54,14.267C4.344,14.267 4.155,14.27 3.925,14.281L3.899,14.189C4.281,13.844 4.632,13.489 4.943,12.863C6.391,10.09 7.454,9.176 9.416,9.168C11.493,9.176 12.548,10.394 13.755,12.612C14.166,13.367 14.484,13.878 14.895,14.252C14.736,14.244 14.54,14.252 14.336,14.27C12.352,14.415 11.293,15.87 9.408,15.87ZM16.424,13.963C16.247,13.807 16.076,13.608 15.932,13.348C16.528,13.145 16.924,12.663 17.105,11.79C17.335,10.623 17.924,10.05 18.879,10.05C19.856,10.05 20.663,10.972 20.663,12.319C20.663,12.66 19.916,12.974 19.175,12.974C17.816,12.974 17.131,13.422 16.424,13.963ZM19.012,11.982C19.323,11.982 19.582,11.723 19.582,11.412C19.582,11.097 19.323,10.842 19.012,10.842C18.697,10.842 18.442,11.097 18.442,11.412C18.442,11.723 18.697,11.982 19.012,11.982ZM9.364,12.73C11.367,12.73 12.851,11.805 13.74,10.864L13.207,9.913C12.193,11.035 10.852,11.649 9.364,11.649C7.853,11.649 6.513,11.042 5.517,9.909L4.988,10.868C5.869,11.801 7.354,12.73 9.364,12.73ZM6.935,15.614C7.587,14.94 8.201,13.434 8.268,12.438L7.328,12.149C7.287,12.941 6.639,14.552 5.976,15.189L6.935,15.614ZM11.893,15.614L12.855,15.189C12.189,14.548 11.541,12.945 11.5,12.149L10.56,12.43C10.597,13.419 11.215,14.922 11.893,15.614ZM3.907,18.024C5.195,18.024 6.484,17.476 7.476,16.451L6.476,15.884C5.491,16.673 4.71,17.014 3.977,17.014C3.551,17.014 3.325,16.869 3.325,16.658C3.325,16.507 3.455,16.399 3.755,16.318C4.04,16.233 5.151,15.907 5.676,15.573L4.74,15.229C4.366,15.392 3.87,15.466 3.455,15.611C2.792,15.811 2.159,16.062 2.159,16.832C2.159,17.58 2.889,18.024 3.907,18.024ZM14.736,18.024C15.754,18.024 16.487,17.584 16.487,16.832C16.487,15.996 15.762,15.748 15.021,15.551C14.636,15.448 14.229,15.374 13.903,15.233L12.97,15.577C13.496,15.907 14.603,16.24 14.892,16.318C15.191,16.399 15.317,16.507 15.317,16.662C15.317,16.873 15.091,17.017 14.666,17.017C13.933,17.017 13.151,16.677 12.17,15.884L11.171,16.451C12.163,17.447 13.455,18.024 14.736,18.024Z"
android:fillColor="#909090"/>
</vector>

View file

@ -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<TransactionFee>? {
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<CryptoCurrency.Token>) {
if (tokens.isEmpty()) return

View file

@ -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<Throwable, Unit>
/**
* 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<TransactionFee>?
}

1
domain/transaction/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.transaction.error
sealed class GetFeeError {
object DataError : GetFeeError()
}

View file

@ -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<Either<GetFeeError.DataError, TransactionFee>> {
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,
),
)
},
)
}

View file

@ -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)

View file

@ -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
}

View file

@ -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<FeeSelectorState> = MutableStateFlow(FeeSelectorState.Empty),
val isSubtract: MutableStateFlow<Boolean> = MutableStateFlow(false),
val receivedAmount: MutableStateFlow<String> = MutableStateFlow(""),
) : SendStates()
// todo [REDACTED_JIRA]

View file

@ -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()
}
}
}

View file

@ -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<List<SendTextField.CustomFee>> = MutableStateFlow(emptyList()),
) : FeeSelectorState()
}
enum class FeeType {
SLOW,
MARKET,
FAST,
CUSTOM,
}

View file

@ -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<AppCurrency>,
) : Converter<Fee, MutableStateFlow<List<SendTextField.CustomFee>>> {
override fun convert(value: Fee): MutableStateFlow<List<SendTextField.CustomFee>> {
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,
),
),
),
)
}
}

View file

@ -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<CryptoCurrencyStatus>,
) : Converter<Unit, SendStates.FeeState> {
override fun convert(value: Unit): SendStates.FeeState {
return SendStates.FeeState(
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
)
}
}

View file

@ -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]*/ }
}
}

View file

@ -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

View file

@ -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<List<SendTextField.CustomFee>>,
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,
)
}
}
}

View file

@ -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,
),
)
}
}
}

View file

@ -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<FeeSelectorState>,
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

View file

@ -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<String>,
isSubtract: StateFlow<Boolean>,
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,
)
}
}
}

View file

@ -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) {

View file

@ -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
}

View file

@ -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"

View file

@ -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

View file

@ -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