Updated on 2026-08-14
This commit is contained in:
parent
31d5c67091
commit
d214ec6ec6
33 changed files with 963 additions and 21 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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]*/ }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue